#!/usr/bin/env python3
# -*- coding: utf-8 -*-

'''
使用须知README
1.安装Python环境
确保电脑已安装Python3（建议3.7及以上版本），并配置好环境变量。
2.安装依赖的Python第三方库
在命令行（CMD或PowerShell）进入脚本所在目录，执行：pip install pysrt ffmpeg-python
3.安装ffmpeg工具
    (1) ffmpeg下载地址：https://www.gyan.dev/ffmpeg/builds/
    (2) 解压文件到一个目录（如C:\ffmpeg）
    (3) 将bin文件夹路径（如C:\ffmpeg\bin）添加到系统环境变量PATH中
4.运行此脚本
双击打开或在命令行中输入python <脚本路径>回车
'''

# 标准库导入
import os
import tempfile
import subprocess
import json
import tkinter as tk
from tkinter import filedialog, messagebox, ttk, scrolledtext
import threading
import queue
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor
from difflib import SequenceMatcher

# 第三方库导入
import pysrt
import ffmpeg
import sys

# 自动检测、下载和配置FFmpeg已经移至setup_ffmpeg.py中

# 导入FFmpeg配置工具
from FFmpegConfigurator import FFmpegConfigurator

# 使用FFmpeg配置工具初始化FFmpeg环境
success = FFmpegConfigurator.setup_ffmpeg()
if not success:
    sys.exit(1)

# 全局常量和配置变量定义
# 检测是否被打包成exe（使用PyInstaller等工具）
IS_FROZEN = getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS')

# DEBUG模式设置（在打包成exe时可以通过环境变量控制）
DEBUG_MODE = os.environ.get('VIDEO_CLIPPER_DEBUG', 'False').lower() == 'true'

# 根据是否打包和DEBUG模式自动调整设置
# 只有在打包成单个exe包并且处于非DEBUG状态时，才不弹出命令行窗口
PRINT_FFMPEG_OUTPUT = not IS_FROZEN or DEBUG_MODE  # 打包后不显示FFmpeg输出，除非DEBUG模式
ALLOW_CONSOLE_WINDOWS = not IS_FROZEN or DEBUG_MODE  # 打包后不弹出控制台窗口，除非DEBUG模式
SYMBOL_SUCCESS = '✓'  # 成功符号
SYMBOL_WARNING = '⚠'  # 警告符号
SYMBOL_ERROR = '✗'    # 错误符号
SYMBOL_CHECKED = '☑'  # 选中符号
SYMBOL_UNCHECKED = '☐'  # 未选中符号
WINDOW_SIZE = 10 # 默认跨屏数

# 辅助函数：封装ffmpeg.probe调用，确保控制台窗口控制

def safe_ffmpeg_probe(file_path):
    """安全地调用ffmpeg.probe，确保能够读取视频内置字幕"""
    try:
        # 根据配置决定是否隐藏控制台窗口
        creation_flags = 0
        if not ALLOW_CONSOLE_WINDOWS:
            creation_flags = subprocess.CREATE_NO_WINDOW  # Windows专用，避免弹出控制台窗口
        
        # 使用subprocess调用ffprobe而不是ffmpeg.probe()来支持控制台窗口隐藏
        cmd = [
            'ffprobe',
            '-v', 'quiet',
            '-print_format', 'json',
            '-show_format',
            '-show_streams',
            file_path
        ]
        
        # 使用二进制模式捕获输出，然后手动解码
        process = subprocess.Popen(
            cmd,
            stdout=subprocess.PIPE,  # 捕获输出
            stderr=subprocess.DEVNULL,  # 不捕获错误
            creationflags=creation_flags
        )
        
        # 等待进程完成并获取输出（二进制模式）
        stdout_bytes, _ = process.communicate(timeout=30)  # 30秒超时
        
        # 手动解码输出，使用错误处理
        stdout = stdout_bytes.decode('utf-8', errors='replace')
        
        if process.returncode != 0:
            raise ffmpeg.Error("ffprobe failed", "", process.returncode)
            
        # 直接解析JSON输出（已经是文本模式）
        return json.loads(stdout)
    except Exception as e:
        # 重新抛出异常，保留原始错误信息
        raise e


class VideoClipper:
    """视频剪辑核心业务逻辑类
    
    负责处理视频字幕提取、搜索匹配和视频片段剪辑的核心功能。
    包含字幕缓存机制以提高性能，支持智能相似度计算和模糊搜索。
    """
    
    def __init__(self):
        self.result_queue = queue.Queue()  # 用于存储处理结果的队列
        self.subtitle_cache = {}  # 缓存已处理的视频字幕，提高重复查询性能
        self.cache_lock = threading.Lock()  # 保护缓存访问的线程安全锁

    def get_cached_subtitles(self, video_path):
        """获取缓存的字幕内容"""
        with self.cache_lock:
            return self.subtitle_cache.get(video_path)

    def cache_subtitles(self, video_path, subtitles):
        """缓存字幕内容"""
        with self.cache_lock:
            self.subtitle_cache[video_path] = subtitles
    
    def extract_subtitles(self, video_path, temp_dir, subtitle_stream_index=None):
        """从视频中提取内嵌字幕流
        
        Args:
            video_path (str): 视频文件路径
            temp_dir (str): 临时目录路径，用于存储提取的字幕文件
            subtitle_stream_index (int, optional): 指定要提取的字幕流索引，
                如果为None，则使用第一个字幕流
            
        Returns:
            tuple: (字幕文件路径, 错误信息)
                - 如果成功，返回 (字幕文件路径, None)
                - 如果失败，返回 (None, 错误信息字符串)
        """
        try:
            subtitle_path = os.path.join(temp_dir, "extracted_subtitle.srt")
            
            # 探测视频流信息，查找所有字幕流
            probe = safe_ffmpeg_probe(video_path)
            subtitle_streams = [stream for stream in probe['streams'] 
                               if stream['codec_type'] == 'subtitle']
            
            if not subtitle_streams:
                return None, "未找到内嵌字幕"
            
            # 确定要提取的字幕流索引
            if subtitle_stream_index is not None:
                # 检查指定的索引是否有效
                valid_indices = [stream['index'] for stream in subtitle_streams]
                if subtitle_stream_index not in valid_indices:
                    return None, f"指定的字幕流索引 {subtitle_stream_index} 无效"
                stream_index = subtitle_stream_index
            else:
                # 默认使用第一个字幕流
                stream_index = subtitle_streams[0]['index']
                
            # 根据配置决定是否隐藏控制台窗口
            creation_flags = 0
            if not ALLOW_CONSOLE_WINDOWS:
                creation_flags = subprocess.CREATE_NO_WINDOW  # Windows专用，避免弹出控制台窗口
            
            # 使用subprocess.Popen而不是ffmpeg.run()来支持控制台窗口隐藏
            cmd = [
                'ffmpeg',
                '-i', video_path,
                '-map', f'0:{stream_index}',
                '-c', 'copy',
                '-y',  # 覆盖输出文件
                subtitle_path
            ]
            
            process = subprocess.Popen(
                cmd,
                stdout=subprocess.DEVNULL,  # 不捕获输出
                stderr=subprocess.DEVNULL,  # 不捕获错误
                creationflags=creation_flags
            )
            
            # 等待进程完成
            process.wait(timeout=300)  # 5分钟超时
            
            if process.returncode != 0:
                return None, f"FFmpeg提取失败，返回代码: {process.returncode}"
            
            return subtitle_path, None
        except ffmpeg.Error as e:  # 捕获FFmpeg特定异常
            # 提取并显示stderr信息
            error_msg = f"FFmpeg提取失败: {str(e)}"
            if hasattr(e, 'stderr') and e.stderr:
                try:
                    error_msg += f"\n详细错误: {e.stderr.decode('utf-8', errors='replace')}"
                except:
                    error_msg += f"\n无法解析stderr内容"
            return None, error_msg
        except FileNotFoundError:  # 捕获文件不存在异常
            return None, f"视频文件未找到: {video_path}"
        except Exception as e:  # 兜底捕获其他异常
            return None, f"提取字幕时出错: {str(e)}"
    
    @staticmethod
    def get_video_duration(video_path):
        """获取视频文件的时长（秒）
        
        Args:
            video_path (str): 视频文件路径
            
        Returns:
            float or None: 视频时长（秒），如果获取失败则返回None
        """
        try:
            probe = safe_ffmpeg_probe(video_path)
            # 从format部分获取duration
            duration = float(probe['format']['duration'])
            return duration
        except (ffmpeg.Error, KeyError, ValueError):
            return None
            
    @staticmethod
    def check_subtitle_video_match(video_path, subtitle_path):
        """检查外挂字幕与视频是否匹配
        
        Args:
            video_path (str): 视频文件路径
            subtitle_path (str): 外挂字幕文件路径
            
        Returns:
            tuple: (是否匹配, 匹配信息)
        """
        try:
            # 解析字幕文件获取最后一个字幕的时间
            try:
                # 首先尝试使用默认编码打开
                subs = pysrt.open(subtitle_path)
            except UnicodeDecodeError:
                # 如果默认编码失败，尝试使用常见编码
                encodings = ['utf-8', 'gbk', 'gb2312', 'big5', 'latin-1']
                subs = None
                for encoding in encodings:
                    try:
                        subs = pysrt.open(subtitle_path, encoding=encoding)
                        break
                    except UnicodeDecodeError:
                        continue
                if subs is None:
                    return False, f"无法解码字幕文件: {subtitle_path}"
            
            if not subs:
                return False, "字幕文件为空"
            
            # 获取视频时长
            video_duration = VideoClipper.get_video_duration(video_path)
            if video_duration is None:
                return False, "无法获取视频时长"
            
            # 获取最后一个字幕的结束时间（秒）
            last_sub = subs[-1]
            last_end = last_sub.end.to_time()
            last_end_seconds = VideoClipper.time_to_seconds(last_end)
            
            # 定义时间轴匹配的最大允许误差（秒）
            TIME_MATCH_THRESHOLD = 60
            # 检查字幕时间是否在视频时长范围内
            time_diff = video_duration - last_end_seconds
            if abs(time_diff) > TIME_MATCH_THRESHOLD:
                return False, f"字幕时间轴与视频不匹配（视频时长: {video_duration:.1f}秒, 字幕结束: {last_end_seconds:.1f}秒）"
            
            return True, f"字幕时间轴与视频匹配良好（视频时长: {video_duration:.1f}秒, 字幕结束: {last_end_seconds:.1f}秒）"
        except Exception as e:
            return False, f"检查字幕匹配时出错: {str(e)}"

    @staticmethod
    def _generate_cache_key(video_path, subtitle_source="internal", external_subtitle_path=None, subtitle_stream_index=None):
        """生成缓存键
        
        根据视频路径、字幕源类型、外挂字幕路径和字幕流索引生成唯一的缓存键。
        
        Args:
            video_path (str): 视频文件路径
            subtitle_source (str): 字幕源类型，"internal"(内置)或"external"(外挂)
            external_subtitle_path (str): 外挂字幕文件路径
            subtitle_stream_index (int, optional): 指定的字幕流索引
            
        Returns:
            str: 唯一的缓存键字符串
        """
        cache_key = f"{video_path}::{subtitle_source}"
        if subtitle_source == "internal" and subtitle_stream_index is not None:
            cache_key = f"{cache_key}::stream_{subtitle_stream_index}"
        if subtitle_source == "external" and external_subtitle_path:
            cache_key = f"{cache_key}::{external_subtitle_path}"
        return cache_key
        

        

            
    @staticmethod
    def _parse_subtitles(subtitle_path):
        """解析字幕文件为列表格式
        
        使用pysrt库解析SRT格式的字幕文件，将每个字幕条目转换为包含
        开始时间、结束时间和文本内容的字典。
        
        Args:
            subtitle_path (str): 字幕文件路径
            
        Returns:
            list: 解析后的字幕列表，每个元素为包含start、end和text键的字典
        """
        try:
            # 首先尝试使用默认编码打开
            subs = pysrt.open(subtitle_path)
        except UnicodeDecodeError:
            # 如果默认编码失败，尝试使用常见编码
            encodings = ['utf-8', 'gbk', 'gb2312', 'big5', 'latin-1']
            subs = None
            for encoding in encodings:
                try:
                    subs = pysrt.open(subtitle_path, encoding=encoding)
                    break
                except UnicodeDecodeError:
                    continue
            if subs is None:
                raise ValueError(f"无法解码字幕文件: {subtitle_path}")
        
        subtitles_list = []
        for sub in subs:
            subtitles_list.append({
                'start': sub.start.to_time(),  # 字幕开始时间
                'end': sub.end.to_time(),      # 字幕结束时间
                'text': sub.text.replace('\n', ' ')  # 字幕文本内容，将换行符替换为空格
            })
        return subtitles_list
    
    def extract_and_search_subtitles(self, video_path, search_text, threshold=0.7, window_size=5, subtitle_source="internal", external_subtitle_path=None):
        """提取和搜索字幕
        
        根据指定的字幕源（视频内置或外挂字幕文件）提取字幕，然后搜索匹配项。
        
        Args:
            video_path (str): 视频文件路径
            search_text (str): 搜索文本
            threshold (float): 相似度阈值
            window_size (int): 跨字幕搜索窗口大小
            subtitle_source (str): 字幕源类型，可选值为"internal"或"external"
            external_subtitle_path (str): 外挂字幕文件路径
            
        Returns:
            tuple: (匹配结果, 错误信息)
        """
        # 生成缓存键
        cache_key = VideoClipper._generate_cache_key(video_path, subtitle_source, external_subtitle_path)
            
        # 检查缓存
        cached = self.get_cached_subtitles(cache_key)
        if cached is not None:
            matches = self.search_subtitles_universal(cached, search_text, threshold, window_size=window_size)
            print(matches)
            if matches: return matches[0][1], None
            else: return None, None
        
        try:
            subtitles_list = None
            
            if subtitle_source == "external" and external_subtitle_path:
                # 使用外挂字幕文件
                if not os.path.exists(external_subtitle_path):
                    return None, f"外挂字幕文件不存在: {external_subtitle_path}"
                
                subtitles_list = VideoClipper._parse_subtitles(external_subtitle_path)
            else:
                # 使用视频内置字幕
                with tempfile.TemporaryDirectory() as temp_dir:
                    subtitle_path, error = self.extract_subtitles(video_path, temp_dir)
                    if error:
                        return None, error
                    
                    subtitles_list = VideoClipper._parse_subtitles(subtitle_path)
            
            # 缓存字幕
            self.cache_subtitles(cache_key, subtitles_list)
            
            # 搜索匹配项
            matches = self.search_subtitles_universal(subtitles_list, search_text, threshold, window_size=window_size)
            print(matches)
            if matches: return matches[0][1], None
            else: return None, None
        except Exception as e:
            return None, f"处理字幕时出错: {str(e)}"
    
    @staticmethod
    def calculate_similarity_intelligent(search_text, target_text):
        """智能相似度计算方法，根据搜索词长度采用不同的匹配策略
        
        针对不同长度的搜索词使用优化的匹配算法：
        - 短搜索词(≤3字符)：使用精确包含、前缀匹配和宽松匹配
        - 中等长度搜索词(4-10字符)：结合SequenceMatcher与语义奖励
        - 长搜索词(>10字符)：依赖SequenceMatcher并添加单词顺序匹配奖励
        
        Args:
            search_text (str): 搜索关键词
            target_text (str): 目标文本（字幕内容）
            
        Returns:
            float: 相似度得分，范围0.0-1.0
        """

        search_text = search_text.lower().strip()
        target_text = target_text.lower().strip()
        
        search_len = len(search_text)
        
        # 1. 短搜索词特殊处理（≤3字符）
        if search_len <= 3:
            # 精确包含检查
            if search_text in target_text:
                return 1.0  # 最高相似度
            
            # 前缀匹配检查
            words = target_text.split()
            for word in words:
                if word.startswith(search_text):
                    return 0.9  # 高相似度
            
            # 对于超短搜索词（1-2字符），使用更宽松的匹配
            if search_len <= 2:
                # 检查是否为单词的一部分
                for word in words:
                    if search_text in word:
                        return 0.7
            
            return 0.0
        
        # 2. 中等长度搜索词（4-10字符）
        elif search_len <= 10:
            base_similarity = SequenceMatcher(None, search_text, target_text).ratio()
            
            # 包含性奖励 - 如果完全包含则给予额外分数
            contains_bonus = 0.3 if search_text in target_text else 0.0
            
            # 单词匹配奖励 - 如果有共同单词则给予额外分数
            search_words = set(search_text.split())
            target_words = set(target_text.split())
            word_match_bonus = 0.2 if search_words.intersection(target_words) else 0.0
            
            return min(1.0, base_similarity + contains_bonus + word_match_bonus)
        
        # 3. 长搜索词（>10字符）
        else:
            # 对于长搜索词，主要依赖SequenceMatcher计算基础相似度
            base_similarity = SequenceMatcher(None, search_text, target_text).ratio()
            
            # 包含性奖励（较小权重）
            contains_bonus = 0.1 if search_text in target_text else 0.0
            
            # 单词顺序匹配奖励 - 对于多词搜索，保持顺序匹配更重要
            search_words = search_text.split()
            target_words = target_text.split()
            
            order_match_bonus = 0.0
            if len(search_words) > 1:
                # 检查单词顺序是否匹配
                search_str = ' '.join(search_words)
                if search_str in target_text:
                    order_match_bonus = 0.15
            
            return min(1.0, base_similarity + contains_bonus + order_match_bonus)

    def search_subtitles_universal(self, subtitle_list, search_text, threshold=0.7, max_results=5, window_size=5):
        """通用模糊搜索，智能处理所有长度的搜索词
        
        实现双层搜索策略：
        1. 单条字幕搜索：在每条字幕中查找匹配
        2. 跨字幕搜索：在指定大小的字幕窗口中查找匹配
        
        结果按相似度排序并限制返回数量。
        
        Args:
            subtitle_list (list): 字幕列表，每个元素为包含start、end、text的字典
            search_text (str): 搜索关键词
            threshold (float, optional): 相似度阈值，默认为0.7
            max_results (int, optional): 最大返回结果数，默认为5
            window_size (int, optional): 跨字幕搜索的窗口大小，默认为5
            
        Returns:
            list: 搜索结果列表，每个元素为(相似度得分, 结果信息)的元组
                结果信息包含start、end、text、match_type等字段
        """

        if not search_text or not search_text.strip():
            return []
        
        search_text = search_text.strip()
        results = []
        
        # 动态调整阈值 - 根据搜索词长度优化匹配精度
        search_len = len(search_text)
        dynamic_threshold = threshold
        # 注释部分为未来可能的阈值优化策略
        '''        
        if search_len <= 3:
            dynamic_threshold = 0.5  # 短搜索词更宽松
        elif search_len <= 6:
            dynamic_threshold = 0.6  # 中等搜索词适中
        '''
        
        # 单条字幕搜索 - 在每条字幕独立查找匹配
        for i, sub in enumerate(subtitle_list):
            similarity = VideoClipper.calculate_similarity_intelligent(search_text, sub['text'])
            
            if similarity >= dynamic_threshold:
                results.append((similarity, {
                    'start': sub['start'],
                    'end': sub['end'],
                    'text': sub['text'],
                    'match_type': 'single',
                    'search_length': search_len,
                    'threshold_used': dynamic_threshold
                }))
        
        # 跨字幕搜索 - 支持跨越多条字幕的长句子搜索，尝试不同窗口大小
        # 存储每个起始位置的最佳匹配结果
        best_matches = {}
        
        # 遍历所有可能的起始位置
        for start_idx in range(len(subtitle_list)):
            # 计算当前起始位置可以使用的最大窗口大小
            max_possible_window = min(window_size, len(subtitle_list) - start_idx)
            
            # 从1到max_possible_window尝试不同窗口大小
            for current_window in range(1, max_possible_window + 1):
                window_subs = subtitle_list[start_idx:start_idx + current_window]
                window_text = ' '.join(sub['text'] for sub in window_subs)
                
                similarity = VideoClipper.calculate_similarity_intelligent(search_text, window_text)
                
                if similarity >= dynamic_threshold:
                    # 检查是否已经为这个起始位置记录了匹配
                    if start_idx not in best_matches or \
                       (similarity > best_matches[start_idx][0] or \
                        (similarity == best_matches[start_idx][0] and current_window < best_matches[start_idx][1]['window_size'])):
                        # 记录更好的匹配（更高的相似度，或相同相似度但更短的窗口）
                        best_matches[start_idx] = (similarity, {
                            'start': window_subs[0]['start'],
                            'end': window_subs[-1]['end'],
                            'text': window_text,
                            'match_type': 'cross',
                            'window_size': current_window,
                            'search_length': search_len,
                            'threshold_used': dynamic_threshold
                        })
        
        # 将所有最佳匹配添加到结果列表
        for similarity, match_info in best_matches.values():
            results.append((similarity, match_info))
        
        # 按用户要求的优先级排序：单条匹配>跨字幕匹配（窗口大小小的）>跨字幕匹配（窗口大小大的）
        # 由于使用reverse=True，将单条匹配设为1，跨字幕匹配设为0，确保单条匹配排在前面
        results.sort(key=lambda x: (x[0], 1 if x[1]['match_type'] == 'single' else 0, -x[1].get('window_size', 1000)), reverse=True)
        return results[:max_results]

    @staticmethod
    def time_to_seconds(t):
        """将datetime.time对象转换为秒数"""
        return t.hour * 3600 + t.minute * 60 + t.second + t.microsecond / 1e6
    
    def extract_video_segment(self, video_path, start_time, end_time, output_path):
        """异步提取视频片段"""
        cmd = [
            'ffmpeg', '-ss', str(start_time), '-to', str(end_time), '-i', video_path,
            '-map', '0:v:0', '-map', '0:a?', '-map', '0:s?',
            '-c:v', 'copy', '-c:a', 'copy', '-c:s', 'copy',
            output_path, '-y'
        ]
        
        try:
            # 根据配置决定是否创建控制台窗口
            creation_flags = 0
            if not ALLOW_CONSOLE_WINDOWS:
                creation_flags = subprocess.CREATE_NO_WINDOW  # Windows专用，避免弹出控制台窗口
                
            # 根据环境决定是否捕获输出
            stdout_target = subprocess.PIPE if PRINT_FFMPEG_OUTPUT else subprocess.DEVNULL
            stderr_target = subprocess.PIPE if PRINT_FFMPEG_OUTPUT else subprocess.DEVNULL
            
            process = subprocess.Popen(
                cmd, 
                stdout=stdout_target,  # 根据环境决定是否捕获输出
                stderr=stderr_target,  # 根据环境决定是否捕获错误
                text=PRINT_FFMPEG_OUTPUT,  # 仅在开发环境以文本模式处理
                encoding='utf-8' if PRINT_FFMPEG_OUTPUT else None,  # 仅在开发环境指定编码
                creationflags=creation_flags
            )
            
            if PRINT_FFMPEG_OUTPUT:
                # 开发环境：捕获并打印输出
                stdout, stderr = process.communicate(timeout=300)
                if stdout:
                    print(f"FFmpeg输出: {stdout}")
                if stderr:
                    print(f"FFmpeg错误: {stderr}")
                
                if process.returncode == 0:
                    return True, None
                else:
                    return False, f"FFmpeg进程返回错误代码: {process.returncode}\n{stderr}"
            else:
                # 打包环境：不捕获输出，等待完成
                process.wait(timeout=300)
                
                if process.returncode == 0:
                    return True, None
                else:
                    return False, f"FFmpeg进程返回错误代码: {process.returncode}"
                
        except subprocess.TimeoutExpired:
            process.kill()
            return False, "视频提取超时"
        except Exception as e:
            return False, f"启动FFmpeg进程失败: {e}"
    
    def _limit_filename_length(self, filename, search_text, ext, max_added_chars=20, max_total_length=255, 
                              task_index=None, total_tasks=None, timestamp=None):
        """限制文件名长度，确保符合操作系统限制
        
        Args:
            filename: 原始文件名（不含扩展名）
            search_text: 搜索文本
            ext: 文件扩展名
            max_added_chars: 相对于原文件名最多增加的字符数
            max_total_length: 文件名总长度上限
            task_index: 任务序号（从1开始）
            total_tasks: 总任务数量
            timestamp: 时间戳
        
        Returns:
            tuple: (limited_filename, limited_search_text)
        """
        # 首先处理搜索文本，确保安全
        invalid_chars = '<>:"/\\|?*'
        safe_search_text = "".join(c for c in search_text if c not in invalid_chars)
        safe_search_text = "".join(c for c in safe_search_text if c.isalnum() or c in (' ', '-', '_')).rstrip()
        if not safe_search_text:
            safe_search_text = "clip"
        
        # 计算额外前缀和后缀的长度
        extra_prefix_length = 0
        extra_suffix_length = 0
        
        # 计算序号前缀长度
        if task_index is not None and total_tasks is not None:
            max_digits = len(str(total_tasks))
            formatted_index = str(task_index).zfill(max_digits)
            extra_prefix_length = len(formatted_index) + 1  # 序号加下划线
        
        # 计算时间戳后缀长度
        if timestamp:
            extra_suffix_length = len(timestamp) + 1  # 时间戳加下划线
        
        # 计算当前文件名长度（不含扩展名部分）
        current_name_length = len(filename)
        
        # 限制搜索文本长度：最多增加max_added_chars个字符
        max_search_text_length = min(len(safe_search_text), max_added_chars)
        limited_search_text = safe_search_text[:max_search_text_length]
        
        # 构建基础文件名并检查总长度限制（包含额外前缀和后缀）
        base_name = f"{filename}({limited_search_text})"
        total_base_length = len(base_name) + extra_prefix_length + extra_suffix_length
        
        # 如果总长度超过限制，进一步截断
        if total_base_length + len(ext) > max_total_length:
            # 计算允许的文件名主体长度（考虑额外前缀和后缀）
            max_base_length = max_total_length - len(ext) - extra_prefix_length - extra_suffix_length
            
            # 计算允许的括号内内容长度
            max_bracket_length = max_base_length - len(filename) - 2  # 2是括号
            
            if max_bracket_length <= 0:
                # 如果连基本文件名都放不下，使用最短命名
                return f"clip{datetime.now().strftime('%H%M%S')}", "clip"
            
            # 进一步限制搜索文本长度
            limited_search_text = limited_search_text[:max_bracket_length]
            
            # 如果搜索文本被截断后仍然太长，还需要截断文件名
            proposed_name = f"{filename}({limited_search_text})"
            if len(proposed_name) + extra_prefix_length + extra_suffix_length + len(ext) > max_total_length:
                # 需要同时截断文件名和搜索文本
                max_total_base = max_total_length - len(ext) - extra_prefix_length - extra_suffix_length - 2  # 2是括号
                
                # 按比例分配长度
                filename_ratio = len(filename) / (len(filename) + len(limited_search_text))
                max_filename_len = int(max_total_base * filename_ratio)
                max_search_len = max_total_base - max_filename_len
                
                limited_filename = filename[:max_filename_len]
                limited_search_text = limited_search_text[:max_search_len]
            else:
                limited_filename = filename
        else:
            limited_filename = filename
        
        return limited_filename, limited_search_text

    def _build_output_filename(self, base_filename, search_text, ext, task_index=None, total_tasks=None, timestamp=None):
        """构建输出文件名，处理序号和时间戳逻辑
        
        Args:
            base_filename: 基础文件名（不含扩展名）
            search_text: 搜索文本
            ext: 文件扩展名
            task_index: 任务序号
            total_tasks: 总任务数
            timestamp: 时间戳
            
        Returns:
            str: 完整的输出文件名
        """
        # 使用文件名长度限制函数
        limited_filename, limited_search_text = self._limit_filename_length(
            base_filename, search_text, ext,
            task_index=task_index,
            total_tasks=total_tasks,
            timestamp=timestamp
        )
        
        # 构建文件名 - 包含序号信息以便排序
        if task_index is not None and total_tasks is not None:
            max_digits = len(str(total_tasks))
            formatted_index = str(task_index).zfill(max_digits)
            if timestamp:
                return f"{limited_filename}_{formatted_index}({limited_search_text}){timestamp}{ext}"
            else:
                return f"{limited_filename}_{formatted_index}({limited_search_text}){ext}"
        else:
            if timestamp:
                return f"{limited_filename}({limited_search_text}){timestamp}{ext}"
            else:
                return f"{limited_filename}({limited_search_text}){ext}"

    def process_clip_task(self, video_path, search_text, result_queue, threshold=0.7, window_size=5, subtitle_source="internal", external_subtitle_path=None, task_index=None, total_tasks=None):
        """处理单个剪辑任务
        
        Args:
            video_path: 视频文件路径
            search_text: 搜索文本
            result_queue: 结果队列
            threshold: 匹配阈值
            window_size: 窗口大小
            subtitle_source: 字幕来源
            external_subtitle_path: 外挂字幕文件路径
            task_index: 任务序号（从1开始）
            total_tasks: 总任务数量
        """
        try:
            base_path = os.path.dirname(video_path)
            filename = os.path.splitext(os.path.basename(video_path))[0]
            ext = os.path.splitext(video_path)[1]
            
            # 使用新的文件名构建函数
            safe_output_name = self._build_output_filename(
                filename, search_text, ext,
                task_index=task_index,
                total_tasks=total_tasks,
                timestamp=None
            )
            
            output_path = os.path.join(base_path, safe_output_name)
            os.makedirs(os.path.dirname(output_path), exist_ok=True)
            
            # 检查文件是否已存在，如果存在则添加时间戳
            if os.path.exists(output_path):
                timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
                output_path = os.path.join(base_path, self._build_output_filename(
                    filename, search_text, ext,
                    task_index=task_index,
                    total_tasks=total_tasks,
                    timestamp=timestamp
                ))
            
            match, error = self.extract_and_search_subtitles(
                video_path, 
                search_text, 
                threshold, 
                window_size,
                subtitle_source,
                external_subtitle_path
            )
            if error:
                result_queue.put(("error", search_text, error))
            elif match:
                segment = match
                start_time = VideoClipper.time_to_seconds(segment['start'])
                end_time = VideoClipper.time_to_seconds(segment['end'])
                print(f"即将提取视频片段，时间范围:({segment['start'].strftime('%H:%M:%S.%f')[:-3]} - {segment['end'].strftime('%H:%M:%S.%f')[:-3]}) ")
                success, error_msg = self.extract_video_segment(video_path, start_time, end_time, output_path)
                if success:
                    result_queue.put(("success", search_text, output_path))
                else:
                    result_queue.put(("error", search_text, error_msg))
            else:
                result_queue.put(("notfound", search_text, None))
        except Exception as e:
            result_queue.put(("error", search_text, f"处理过程中发生未知错误: {str(e)}"))
   
    def extract_screenshot(self, video_path, timestamp, output_path):
        """提取视频指定时间的截图
        
        Args:
            video_path: 视频文件路径
            timestamp: 截图时间（秒）
            output_path: 输出图片路径
            
        Returns:
            tuple: (success, error_message)
        """
        try:
            # 构建FFmpeg命令进行截图
            cmd = [
                'ffmpeg',
                '-ss', str(timestamp),  # 指定截图时间
                '-i', video_path,       # 输入视频文件
                '-vframes', '1',        # 只截取一帧
                '-q:v', '2',            # 高质量截图（1-31，越小质量越高）
                '-y',                   # 覆盖已存在文件
                output_path
            ]
            
            # 执行截图命令（使用二进制模式避免编码问题）
            process = subprocess.Popen(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                creationflags=subprocess.CREATE_NO_WINDOW
            )
            
            # 获取二进制输出
            stdout_bytes, stderr_bytes = process.communicate(timeout=30)
            
            if process.returncode == 0:
                return True, None
            else:
                # 处理可能的编码错误
                try:
                    error_msg = stderr_bytes.decode('utf-8', errors='replace')
                except UnicodeDecodeError:
                    # 如果遇到编码错误，使用原始字节数据
                    error_msg = "截图失败（编码错误）"
                return False, f"截图失败: {error_msg}"
                
        except subprocess.TimeoutExpired:
            process.kill()
            return False, "截图超时"
        except Exception as e:
            return False, f"截图过程中发生错误: {e}"

    def process_screenshot_task(self, video_path, search_text, result_queue, threshold=0.7, window_size=5, subtitle_source="internal", external_subtitle_path=None, task_index=None, total_tasks=None):
        """处理单个截图任务
        
        Args:
            video_path: 视频文件路径
            search_text: 搜索文本
            result_queue: 结果队列
            threshold: 匹配阈值
            window_size: 窗口大小
            subtitle_source: 字幕来源
            external_subtitle_path: 外挂字幕文件路径
            task_index: 任务序号（从1开始）
            total_tasks: 总任务数量
        """
        try:
            base_path = os.path.dirname(video_path)
            filename = os.path.splitext(os.path.basename(video_path))[0]
            
            # 使用新的文件名构建函数
            safe_output_name = self._build_output_filename(
                filename, search_text, '.jpg',
                task_index=task_index,
                total_tasks=total_tasks,
                timestamp=None
            )
            
            output_path = os.path.join(base_path, safe_output_name)
            os.makedirs(os.path.dirname(output_path), exist_ok=True)
            
            # 检查文件是否已存在，如果存在则添加时间戳
            if os.path.exists(output_path):
                timestamp_str = datetime.now().strftime("%Y%m%d%H%M%S")
                output_path = os.path.join(base_path, self._build_output_filename(
                    filename, search_text, '.jpg',
                    task_index=task_index,
                    total_tasks=total_tasks,
                    timestamp=timestamp_str
                ))
            
            # 搜索字幕匹配
            match, error = self.extract_and_search_subtitles(
                video_path, 
                search_text, 
                threshold, 
                window_size,
                subtitle_source,
                external_subtitle_path
            )
            
            if error:
                result_queue.put(("error", search_text, error))
            elif match:
                segment = match
                # 计算字幕中间时间
                start_time = VideoClipper.time_to_seconds(segment['start'])
                end_time = VideoClipper.time_to_seconds(segment['end'])
                middle_time = (start_time + end_time) / 2
                
                print(f"即将截图，时间点: {middle_time:.2f}秒 (字幕时间: {segment['start'].strftime('%H:%M:%S.%f')[:-3]} - {segment['end'].strftime('%H:%M:%S.%f')[:-3]})")
                
                # 执行截图
                success, error_msg = self.extract_screenshot(video_path, middle_time, output_path)
                if success:
                    result_queue.put(("screenshot_success", search_text, output_path))
                else:
                    result_queue.put(("screenshot_error", search_text, error_msg))
            else:
                result_queue.put(("notfound", search_text, None))
                
        except Exception as e:
            result_queue.put(("error", search_text, f"截图过程中发生未知错误: {str(e)}"))

    # 在VideoClipper类中添加清理方法
    def clear_cache(self):
        """清理字幕缓存"""
        with self.cache_lock:
            self.subtitle_cache.clear()

class VideoClipperApp:
    """视频剪辑应用程序GUI类
    
    负责创建和管理视频剪辑工具的用户界面，处理用户交互，
    并调用VideoClipper类的核心业务逻辑完成视频剪辑任务。
    主要功能包括：视频文件选择、字幕搜索条件设置、
    匹配阈值调整、搜索结果展示、批量视频剪辑等。
    """
    
    def __init__(self, root):
        # 丰富的色彩配置
        self.color_bg = '#f0f2f5'  # 柔和的浅灰色背景
        self.color_primary = '#3f51b5'  # 深蓝紫色主色调
        self.color_primary_light = '#e8eaf6'  # 浅蓝色背景
        self.color_primary_dark = '#303f9f'  # 深色主色调
        self.color_success = '#4caf50'  # 绿色（成功状态）
        self.color_warning = '#ff9800'  # 橙色（警告状态）
        self.color_danger = '#f44336'  # 红色（错误状态）
        self.color_info = '#2196f3'  # 蓝色（信息状态）
        self.color_button = '#2196f3'  # 鲜艳的蓝色主按钮颜色
        self.color_button_hover = '#1976d2'  # 深色悬停效果
        self.color_text = '#212121'  # 深灰色文本以确保可读性
        self.color_text_light = '#757575'  # 次要文本颜色
        self.color_entry = '#ffffff'  # 输入框背景色
        self.color_border = '#e0e0e0'  # 浅色边框
        self.color_header = '#3f51b5'  # 标题栏颜色
        self.color_panel = '#ffffff'  # 面板背景色
        
        # 字体设置 - 使用Windows 10默认字体
        self.title_font = ('Segoe UI', 11, 'bold')  # 用于标题
        self.label_font = ('Segoe UI', 10)  # 用于普通标签
        self.button_font = ('Segoe UI', 10, 'bold')  # 用于按钮文本
        self.font_bold = ('Segoe UI', 10, 'bold')  # 用于标题和表头
        self.text_font = ('Consolas', 9)  # 用于代码和文本输入

        self.root = root
        self.root.title("根据字幕剪辑视频小工具")
        self.root.configure(bg=self.color_bg)
        
        # 设置Windows 10风格主题
        self.style = ttk.Style()
        # 尝试使用vista主题（最接近Windows 10风格）
        available_themes = self.style.theme_names()
        if 'vista' in available_themes:
            self.style.theme_use('vista')
        elif 'winnative' in available_themes:
            self.style.theme_use('winnative')
        else:
            self.style.theme_use('clam')
        
        # 配置基础样式
        self.style.configure('.', background=self.color_bg, foreground=self.color_text)
        self.style.configure('TFrame', background=self.color_bg)
        
        # 配置标签样式
        self.style.configure('TLabel', background=self.color_bg, foreground=self.color_text, 
                            font=self.label_font)
        
        # 配置按钮样式
        self.style.configure('TButton', background=self.color_button, foreground='black', 
                            font=self.button_font, focuscolor='none', borderwidth=1)
        
        self.style.map('TButton', 
                      background=[('active', self.color_button_hover), ('pressed', self.color_button_hover)],
                      relief=[('pressed', 'sunken'), ('!pressed', 'raised')])
        
        # 配置进度条样式
        self.style.configure("Horizontal.TProgressbar", 
                           background=self.color_primary,
                           troughcolor=self.color_bg,
                           bordercolor=self.color_border,
                           lightcolor=self.color_primary_light,
                           darkcolor=self.color_primary_dark,
                           thickness=20)
        
        # 配置滚动条样式
        self.style.configure("Vertical.TScrollbar", 
                           background=self.color_bg,
                           troughcolor=self.color_bg,
                           bordercolor=self.color_border,
                           arrowcolor=self.color_text,
                           lightcolor=self.color_bg,
                           darkcolor=self.color_bg)
        self.style.map("Vertical.TScrollbar", 
                      background=[('active', self.color_primary)])

        # 初始化任务跟踪属性
        # 初始化hover_item属性，用于鼠标悬停效果
        self.hover_item = None
        self.total_tasks = 0
        self.completed_tasks = 0
        
        # 初始化编辑状态标志
        self.is_editing = False
        
        # 初始化业务逻辑
        self.clipper = VideoClipper()
        # 动态调整线程池大小：CPU核心数×2
        self.MAX_WORKERS = os.cpu_count() * 2 if os.cpu_count() else 4
        # 创建全局线程池（仅初始化一次）
        self.executor = ThreadPoolExecutor(max_workers=self.MAX_WORKERS)
        
        # 创建UI
        self.setup_ui()
        
        # 居中显示窗口
        self.center_window()
    
    def center_window(self):
        """居中显示窗口"""
        self.root.update_idletasks()
        width = self.root.winfo_width()
        height = self.root.winfo_height()
        x = (self.root.winfo_screenwidth() // 2) - (width // 2)
        y = (self.root.winfo_screenheight() // 2) - (height // 2)
        self.root.geometry(f'{width}x{height}+{x}+{y}')
        
        # 注册窗口关闭事件处理
        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
        
        # 为根窗口绑定点击事件，用于检测点击编辑区域外的情况
        self.root.bind("<Button-1>", self._on_root_click)
    
    def on_closing(self):
        """处理窗口关闭事件，释放资源"""
        try:
            # 关闭线程池，不再接受新任务
            if hasattr(self, 'executor'):
                self.executor.shutdown(wait=False)
        except Exception:
            # 忽略可能的错误，确保程序能够正常退出
            pass
        
        # 销毁窗口
        self.root.destroy()
    
    def setup_ui(self):
        """设置用户界面布局和组件
        
        创建并配置所有UI组件，包括：
        - 主框架布局
        - 视频文件选择区域
        - 搜索表格区域（包含控制按钮、参数调节、表格和滚动条）
        - 操作按钮区域
        - 状态和进度显示区域
        
        同时设置样式、绑定事件处理器。
        """
        # 主容器
        main_frame = ttk.Frame(self.root, padding="15")
        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # 确保ttk样式正确应用 - 使用现有的self.style
        # 基础样式配置，不添加特殊边框或分隔线
        self.style.configure("Treeview",
                           font=self.text_font,
                           rowheight=30,
                           fieldbackground="white",
                           background="white",
                           foreground="black",
                           selectbackground="#e6f2ff",
                           selectforeground="black")
        
        # 配置表头样式
        self.style.configure("Treeview.Heading",
                           font=self.button_font,
                           background="#f0f0f0")
        
        # 设置选中行的样式
        self.style.map("Treeview",
                     background=[('selected', '#e6f2ff')],
                     foreground=[('selected', 'black')])
        
        # 视频文件选择区域 - 紧凑化设计
        file_frame = ttk.LabelFrame(main_frame, text="视频文件", padding="5")
        file_frame.grid(row=0, column=0, columnspan=2, padx=5, pady=5, sticky='ew')
        file_frame.columnconfigure(1, weight=1)
        
        ttk.Label(file_frame, text="选择视频文件：").grid(row=0, column=0, padx=3, pady=3, sticky='w')
        
        self.video_path_var = tk.StringVar()
        
        file_entry = ttk.Entry(file_frame, textvariable=self.video_path_var, state='readonly', font=self.text_font)
        file_entry.grid(row=0, column=1, padx=3, pady=3, sticky='ew')
        
        self.select_file_button = ttk.Button(file_frame, text="浏览", width=6, command=self.select_file)
        self.select_file_button.grid(row=0, column=2, padx=3, pady=3)
        
        # 字幕源选择区域
        subtitle_source_frame = ttk.LabelFrame(file_frame, text="字幕源", padding="5")
        subtitle_source_frame.grid(row=1, column=0, columnspan=3, padx=3, pady=3, sticky='ew')
        subtitle_source_frame.columnconfigure(0, weight=1)
        subtitle_source_frame.columnconfigure(1, weight=1)
        
        self.subtitle_source_var = tk.StringVar(value="internal")
        
        # 视频内置字幕选项
        subtitle_internal_frame = ttk.Frame(subtitle_source_frame)
        subtitle_internal_frame.grid(row=0, column=0, padx=3, pady=3, sticky='w')
        
        self.subtitle_source_internal_radio = ttk.Radiobutton(subtitle_internal_frame, text="视频内置字幕", variable=self.subtitle_source_var, value="internal")
        self.subtitle_source_internal_radio.grid(row=0, column=0, padx=3, pady=3, sticky='w')
        
        # 内置字幕流选择下拉列表
        self.subtitle_stream_var = tk.StringVar()
        self.subtitle_stream_combobox = ttk.Combobox(subtitle_internal_frame, textvariable=self.subtitle_stream_var, state='disabled', width=30)
        self.subtitle_stream_combobox.grid(row=0, column=1, padx=3, pady=3, sticky='w')
        
        # 外挂字幕选项
        subtitle_external_frame = ttk.Frame(subtitle_source_frame)
        # 修改sticky参数为'ew'，使其能够在水平方向上扩展
        subtitle_external_frame.grid(row=0, column=1, padx=3, pady=3, sticky='ew')
        
        self.subtitle_source_external_radio = ttk.Radiobutton(subtitle_external_frame, text="外挂字幕文件", variable=self.subtitle_source_var, value="external")
        self.subtitle_source_external_radio.grid(row=0, column=0, padx=3, pady=3, sticky='w')
        
        self.external_subtitle_var = tk.StringVar()
        
        # 外挂字幕输入框 - 移除固定宽度，使其能够随窗口大小变化
        self.external_subtitle_entry = ttk.Entry(subtitle_external_frame, textvariable=self.external_subtitle_var, state='disabled', font=self.text_font)
        self.external_subtitle_entry.grid(row=0, column=1, padx=3, pady=3, sticky='ew')
        
        # 确保subtitle_external_frame的列1有足够权重，使其能够扩展
        subtitle_external_frame.columnconfigure(1, weight=1)
        
        self.select_subtitle_button = ttk.Button(subtitle_external_frame, text="浏览", width=6, command=self.select_subtitle_file, state='disabled')
        self.select_subtitle_button.grid(row=0, column=2, padx=3, pady=3)
        
        # 绑定字幕源选择变化事件
        self.subtitle_source_var.trace_add("write", self.on_subtitle_source_change)
        
        # 搜索表格区域 - 紧凑化设计
        search_frame = ttk.LabelFrame(main_frame, text="要搜索的字幕（支持在一行内输入多条，每条之间以空行分隔，程序将自动拆分为每条一行）", padding="5")
        search_frame.grid(row=1, column=0, padx=5, pady=5, sticky='nsew')
        search_frame.columnconfigure(0, weight=1)
        search_frame.rowconfigure(1, weight=1)
        
        # 表格控制按钮
        control_frame = ttk.Frame(search_frame)
        control_frame.grid(row=0, column=0, padx=3, pady=3, sticky='ew')
        
        self.add_row_button = ttk.Button(control_frame, text="添加行", command=self.add_row)
        self.add_row_button.pack(side=tk.LEFT, padx=5)
        
        self.select_all_button = ttk.Button(control_frame, text="全选", command=self.select_all)
        self.select_all_button.pack(side=tk.LEFT, padx=5)
        
        self.invert_button = ttk.Button(control_frame, text="反选", command=self.invert_selection)
        self.invert_button.pack(side=tk.LEFT, padx=5)
        
        self.delete_row_button = ttk.Button(control_frame, text="删除行", command=self.delete_row)
        self.delete_row_button.pack(side=tk.LEFT, padx=5)
        
        # 参数调节区域 - 紧凑化
        params_frame = ttk.Frame(control_frame)
        params_frame.pack(side=tk.RIGHT, padx=3)
        
        # 阈值滑块 - 紧凑化
        threshold_frame = ttk.Frame(params_frame)
        threshold_frame.pack(side=tk.LEFT, padx=3)
        
        ttk.Label(threshold_frame, text="阈值:").pack(side=tk.LEFT, padx=3)
        self.threshold_var = tk.DoubleVar(value=0.6)
        self.threshold_slider = ttk.Scale(threshold_frame, from_=0.1, to=1.0, orient=tk.HORIZONTAL, 
                                        variable=self.threshold_var, length=100)
        self.threshold_slider.pack(side=tk.LEFT, padx=3)
        
        self.threshold_label = ttk.Label(threshold_frame, text="{:.2f}".format(self.threshold_var.get()), width=5)
        self.threshold_label.pack(side=tk.LEFT, padx=3)
        
        # 跨屏数输入框 - 紧凑化
        window_size_frame = ttk.Frame(params_frame)
        window_size_frame.pack(side=tk.LEFT, padx=3)
        
        ttk.Label(window_size_frame, text="跨屏:").pack(side=tk.LEFT, padx=3)
        self.window_size_var = tk.IntVar(value=WINDOW_SIZE)
        # 添加验证函数确保跨屏数大于0
        vcmd = (self.root.register(self.validate_window_size), '%P')
        self.window_size_spinbox = ttk.Spinbox(window_size_frame, from_=1, to=20, 
                                             textvariable=self.window_size_var, width=5,
                                             validate='key', validatecommand=vcmd)
        self.window_size_spinbox.pack(side=tk.LEFT, padx=5)
        
        # 更新阈值显示
        self.threshold_slider.bind("<Motion>", self.update_threshold_label)
        self.threshold_slider.bind("<ButtonRelease-1>", self.update_threshold_label)
        
        # 表格 - 紧凑化
        table_frame = ttk.Frame(search_frame)
        table_frame.grid(row=1, column=0, padx=3, pady=3, sticky='nsew')
        table_frame.columnconfigure(0, weight=1)
        table_frame.rowconfigure(0, weight=1)
        
        # 创建Treeview表格
        self.tree = ttk.Treeview(table_frame, columns=('select', 'search_text', 'threshold', 'result'), 
                               show='headings', height=6)
        
        # 在表格初始化时就创建并配置hover标签的样式
        # 这种方式在Windows上通常更可靠
        self.tree.tag_configure('hover', background=self.color_primary_light)
        # 添加不同状态的标签样式
        self.tree.tag_configure('success', background=self.color_primary_light)
        self.tree.tag_configure('warning', background=self.color_primary_light)
        
        # 不再需要行分隔线专用标签，通过Treeview.Item样式实现

        # 配置列
        self.tree.heading('select', text='选择')
        self.tree.heading('search_text', text='搜索内容')
        self.tree.heading('threshold', text='匹配阈值')
        self.tree.heading('result', text='匹配结果')

        # 设置列宽并使其能填充满表格
        self.tree.column('select', width=50, anchor='center', stretch=True)
        self.tree.column('search_text', width=350, anchor='w', stretch=True)
        self.tree.column('threshold', width=100, anchor='center', stretch=True)
        self.tree.column('result', width=350, anchor='w', stretch=True)
        
        # 绑定双击事件用于编辑单元格
        self.tree.bind('<Double-1>', self.on_tree_double_click)
        # 绑定回车事件，在不处于编辑状态时添加新行
        self.tree.bind('<Return>', self.on_tree_enter_press)
        # 绑定鼠标移动事件，用于实现鼠标悬停效果
        self.tree.bind('<Motion>', self.on_tree_mouse_motion)

        # 添加滚动条
        # 垂直滚动条
        tree_scrollbar = ttk.Scrollbar(table_frame, orient='vertical', command=self.tree.yview)
        self.tree.configure(yscrollcommand=tree_scrollbar.set)
        
        # 水平滚动条
        tree_hscrollbar = ttk.Scrollbar(table_frame, orient='horizontal', command=self.tree.xview)
        self.tree.configure(xscrollcommand=tree_hscrollbar.set)
        
        # 布局表格和滚动条
        self.tree.grid(row=0, column=0, sticky='nsew')
        tree_scrollbar.grid(row=0, column=1, sticky='ns')
        tree_hscrollbar.grid(row=1, column=0, sticky='ew')
        
        # 表格操作按钮 - 紧凑化
        action_frame = ttk.Frame(search_frame)
        action_frame.grid(row=2, column=0, padx=3, pady=5, sticky='ew')
        
        # 开始匹配按钮
        self.match_button = ttk.Button(action_frame, text="匹配", command=self.start_matching)
        self.match_button.pack(side=tk.LEFT, padx=3)
        
        # 开始剪辑按钮
        self.clip_button = ttk.Button(action_frame, text="剪辑", command=self.start_clipping)
        self.clip_button.pack(side=tk.LEFT, padx=3)
        
        # 截图按钮
        self.screenshot_button = ttk.Button(action_frame, text="截图", command=self.start_screenshot)
        self.screenshot_button.pack(side=tk.LEFT, padx=3)
        
        # 不确定进度条（放在剪辑按钮右边）
        self.indeterminate_progress = ttk.Progressbar(action_frame, mode='indeterminate', length=80)
        self.indeterminate_progress.pack(side=tk.LEFT, padx=5, pady=2)
        
        # 在剪辑按钮右侧添加一个分隔符框架
        separator_frame = ttk.Frame(action_frame)
        separator_frame.pack(side=tk.LEFT, expand=True, fill=tk.X)
        
        # 进度条和状态标签（靠右对齐，水平排列）
        progress_container = ttk.Frame(action_frame)
        progress_container.pack(side=tk.RIGHT, padx=3)
        
        # 配置网格布局
        progress_container.columnconfigure(0, weight=0)
        progress_container.columnconfigure(1, weight=1)
        progress_container.rowconfigure(0, weight=1)
        
        # 状态标签在左边，移除右侧间距，优化对齐
        self.status_label = ttk.Label(progress_container, text="就绪", width=20, anchor='e')
        self.status_label.grid(row=0, column=0, padx=(5, 0), pady=2, sticky='ew')
        
        # 确定进度条在右边，移除左侧间距，增加长度为原来的2倍
        self.progress = ttk.Progressbar(progress_container, mode='determinate', length=140)
        self.progress.grid(row=0, column=1, padx=(0, 5), pady=2, sticky='ew')
        
        # 配置列权重
        progress_container.columnconfigure(1, weight=1)
        
        # 输出信息区域 - 紧凑化
        output_frame = ttk.LabelFrame(main_frame, text="输出信息", padding="5")
        output_frame.grid(row=3, column=0, padx=5, pady=5, sticky='nsew')
        output_frame.columnconfigure(0, weight=1)
        output_frame.rowconfigure(0, weight=1)
        
        # 信息面板和滚动条 - 紧凑化
        info_frame = ttk.Frame(output_frame)
        info_frame.grid(row=0, column=0, sticky='nsew')
        info_frame.columnconfigure(0, weight=1)
        info_frame.rowconfigure(0, weight=1)
        
        self.info_panel = tk.Text(info_frame, width=60, height=8, state='disabled', 
                                 font=self.text_font, bg=self.color_entry, relief='solid', bd=1, padx=3, pady=3)
        info_scrollbar = ttk.Scrollbar(info_frame, orient='vertical', command=self.info_panel.yview)
        self.info_panel.configure(yscrollcommand=info_scrollbar.set)
        self.info_panel.tag_configure('success', foreground=self.color_success)  # 成功绿色
        self.info_panel.tag_configure('error', foreground=self.color_danger)      # 错误红色
        self.info_panel.tag_configure('warning', foreground=self.color_warning)  # 警告黄色
        self.info_panel.tag_configure('info', foreground=self.color_info)        # 信息蓝色
        
        self.info_panel.grid(row=0, column=0, sticky='nsew')
        info_scrollbar.grid(row=0, column=1, sticky='ns')
        
        # 配置网格权重
        main_frame.columnconfigure(0, weight=1)
        main_frame.rowconfigure(1, weight=1)
        main_frame.rowconfigure(3, weight=1)
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=1)
        
        # 设置最小窗口大小
        self.root.minsize(800, 400)
        # 设置初始窗口大小：宽x高 - 紧凑化
        self.root.geometry('900x650')
        
        # 设置窗口图标 (如果有的话)
        try:
            self.root.iconbitmap(default='')  # 可以指定图标路径
        except:
            pass
        
        # 默认添加一行搜索条件，但不立即进入编辑模式
        self.add_row(edit=False)
        
        # 选中按钮点击事件 - 处理选择列的点击
        self.tree.bind('<Button-1>', self.on_tree_click)
    
    def append_info(self, msg):
        """向信息面板添加带时间戳的消息"""
        timestamp = datetime.now().strftime("[%H:%M:%S]")
        self.info_panel.config(state="normal")
        # 根据消息类型使用不同颜色标签
        if msg.startswith(SYMBOL_SUCCESS):
            self.info_panel.insert("end", f"{timestamp} {msg}\n", 'success')
        elif msg.startswith(SYMBOL_WARNING):
            self.info_panel.insert("end", f"{timestamp} {msg}\n", 'warning')
        elif msg.startswith(SYMBOL_ERROR):
            self.info_panel.insert("end", f"{timestamp} {msg}\n", 'error')
        else:
            self.info_panel.insert("end", f"{timestamp} {msg}\n")
        self.info_panel.see("end")
        self.info_panel.config(state="disabled")
       
    def reset_progress_bar(self):
        """重置进度条到初始状态"""
        self.progress.config(value=0)
        self.indeterminate_progress.stop()
        self.status_label.config(text="就绪")

    def update_threshold_label(self, event=None):
        """更新阈值显示标签并同步到被勾选的行"""
        value = round(self.threshold_var.get(), 2)
        self.threshold_label.config(text=f"{value:.2f}")
        
        # 在滑块滑动过程中就同步更新选中行的阈值
        for item in self.tree.get_children():
            values = list(self.tree.item(item, 'values'))
            if len(values) > 0 and values[0] == SYMBOL_CHECKED:  # 只更新被选中的行
                if len(values) > 2:
                    values[2] = f"{value:.2f}"
                    self.tree.item(item, values=values)
    
    def validate_window_size(self, new_value):
        """验证跨屏数输入是否为正数"""
        # 允许为空（删除所有字符）
        if not new_value:
            return True
        
        try:
            # 尝试转换为整数
            value = int(new_value)
            # 检查是否大于0
            return value > 0
        except ValueError:
            # 不是有效数字
            return False
    
    def add_row(self, edit=True):
        """添加一行搜索条件
        
        添加新的搜索行到表格中，并可选择性地立即进入编辑模式。
        如果当前有行正在编辑，会先取消编辑状态。
        
        Args:
            edit (bool, optional): 是否在添加后立即进入编辑模式，默认为True
            
        Returns:
            str: 新添加行的ID
        """
        # 检查是否有行正在编辑，如果有则取消编辑状态
        if self.is_editing:
            self.is_editing = False
            # 强制清理任何可能存在的编辑器UI元素
            for widget in self.tree.winfo_children():
                if isinstance(widget, ttk.Frame):
                    widget.destroy()
        
        threshold = round(self.threshold_var.get(), 2)
        # 所有新增的行默认为选中状态
        checkbox_state = SYMBOL_CHECKED  # 保留原来的符号用于显示
        # 简化行添加，不再需要交替行标签
        # Treeview.Item样式会自动应用行分隔线
        item_id = self.tree.insert('', 'end', values=(checkbox_state, '', f"{threshold:.2f}", ''))
        # 只有在UI完全初始化后才进入编辑模式
        if edit:
            # 使用after确保UI已渲染
            self.root.after(100, self.edit_cell, item_id, 'search_text')
        return item_id  # 返回新行的ID
    
    def edit_cell(self, item_id, column):
        """编辑表格单元格
        
        根据单元格列类型创建不同的编辑器：
        - search_text列：创建文本编辑器
        - threshold列：创建阈值编辑器
        
        Args:
            item_id (str): 要编辑的行ID
            column (str): 要编辑的列名
        """
        try:
            # 根据列类型创建不同的编辑器
            if column == 'search_text':
                # 获取并验证单元格位置
                x, y, width, height = self._get_validated_cell_position(item_id, column)
                if x is not None:
                    self._create_search_text_editor(item_id, x, y, width, height)
            elif column == 'threshold':
                self._create_threshold_editor(item_id)
        except Exception as error:
            print(f"编辑单元格时出错: {str(error)}")
            
    def _get_validated_cell_position(self, item_id, column):
        """获取并验证单元格位置"""
        # 获取单元格位置
        bbox = self.tree.bbox(item_id, column)
        if not bbox or len(bbox) < 4:  # 检查bbox是否为空或不完整
            # 如果单元格尚未渲染或位置信息不完整，稍后再尝试
            self.root.after(100, self.edit_cell, item_id, column)
            return None, None, None, None
        
        try:
            x, y, width, height = bbox
        except ValueError:
            # 如果解包失败，使用默认尺寸
            x, y, width, height = 0, 0, 200, 100
        
        # 确保宽度和高度为正
        width = max(100, width)
        height = max(50, height)
        
        return x, y, width, height
        
    def _create_search_text_editor(self, item_id, x, y, width, height):
        """创建搜索文本的编辑器
        
        在指定位置创建文本编辑器框架和文本控件，
        设置编辑状态标志，并绑定相关事件。
        
        Args:
            item_id (str): 正在编辑的行ID
            x (int): 编辑器的X坐标
            y (int): 编辑器的Y坐标
            width (int): 编辑器的宽度
            height (int): 编辑器的高度
        """
        # 创建临时编辑框架
        edit_frame = ttk.Frame(self.tree)
        # 保存对应的item_id到编辑框架的属性中
        edit_frame.edit_item_id = item_id
        edit_frame.place(x=x, y=y, width=width, height=height)
        
        # 创建文本控件
        text_widget = self._create_text_widget(edit_frame, item_id)
        
        # 设置编辑状态标志
        self.is_editing = True
        
        # 绑定事件处理
        self._bind_text_editor_events(text_widget, edit_frame, item_id)
        
    def _create_text_widget(self, edit_frame, item_id):
        """创建文本控件并加载当前值"""
        text_widget = scrolledtext.ScrolledText(edit_frame, wrap=tk.WORD, font=self.text_font, 
                                              bd=0, highlightthickness=0)
        text_widget.pack(fill=tk.BOTH, expand=True)
        
        # 获取当前值
        try:
            current_value = self.tree.item(item_id, 'values')[1]
            text_widget.insert(tk.END, current_value)
        except (IndexError, TypeError):
            pass
        
        text_widget.focus_set()
        try:
            text_widget.tag_add("sel", "1.0", "end")
        except TclError:
            pass
        
        return text_widget
        
    def _bind_text_editor_events(self, text_widget, edit_frame, item_id):
        """绑定文本编辑器的事件处理函数"""
        # 当用户输入时实时更新树视图中的内容
        def update_text(event=None):
            try:
                text = text_widget.get("1.0", tk.END).strip()
                values = list(self.tree.item(item_id, 'values'))
                if len(values) >= 2:
                    values[1] = text
                    self.tree.item(item_id, values=values)
            except Exception:
                pass
        
        text_widget.bind("<KeyRelease>", update_text)  # 按键释放时更新
        
        # 保存并关闭编辑器
        def save_with_state_reset(event=None):
            # 注意：这里不再处理文本输入，所有文本处理都在_cleanup_text_editor中统一处理
            self._cleanup_text_editor(edit_frame, item_id)
        
        text_widget.bind("<Return>", save_with_state_reset)  # Enter保存
        text_widget.bind("<Control-Return>", lambda e: None)  # 允许按Enter换行
        text_widget.bind("<Escape>", lambda e: self._cleanup_text_editor(edit_frame, item_id))
        # 失去焦点时取消编辑状态
        text_widget.bind("<FocusOut>", save_with_state_reset)#lambda e: self._cleanup_text_editor(edit_frame, item_id)
        
    def _process_text_input(self, item_id, text):
        """处理文本输入，包括单行输入和批量导入"""
        # 检查是否包含空行（多行输入，用于批量导入）
        if '\n\n' in text:
            self._import_multiple_search_terms(item_id, text)
        else:
            # 单行输入，正常更新
            values = list(self.tree.item(item_id, 'values'))
            if len(values) >= 2:
                values[1] = text
                self.tree.item(item_id, values=values)
        
    def _import_multiple_search_terms(self, item_id, text):
        """批量导入多个搜索词"""
        # 按空行分割文本
        search_terms = [term.strip() for term in text.split('\n\n') if term.strip()]
        
        if not search_terms:
            return
            
        # 更新当前行
        values = list(self.tree.item(item_id, 'values'))
        if len(values) >= 2:
            values[1] = search_terms[0]
            self.tree.item(item_id, values=values)
            
            # 获取当前行的父节点和索引位置
            parent = self.tree.parent(item_id)
            index = self.tree.index(item_id)
            current_threshold = self.threshold_var.get()
            
            # 在当前行后面添加剩余的搜索词作为新行
            for term in search_terms[1:]:
                # 索引位置+1表示在当前行之后添加
                new_item = self.tree.insert(parent, index + 1, 
                                          values=(SYMBOL_CHECKED, term, f"{current_threshold:.2f}", ''))  # 保留原来的符号用于显示
                # 简化批量导入，不再需要交替行标签
                # Treeview.Item样式会自动应用行分隔线
                # 每添加一行，索引加1，确保后续行都在前一行之后
                index += 1
            
            # 显示导入成功信息
            self.append_info(f"{SYMBOL_SUCCESS}成功导入 {len(search_terms)} 个搜索词")
            
    def _cleanup_text_editor(self, edit_frame, item_id):
        """清理文本编辑器，并统一处理文本输入
        
        这是统一的编辑状态清理方法，被多种退出编辑模式的场景调用：
        - 按下回车键
        - 点击表格内空白处
        - 点击表格外空白处
        - 失去焦点时
        - 按下Escape键时
        
        Args:
            edit_frame: 编辑框架控件
            item_id (str): 对应的项目ID
        """
        try:
            # 添加调试信息
            #print('\n--- 进入_cleanup_text_editor ---')
            #print(f'编辑框架ID: {id(edit_frame)}, item_id: {item_id}')
            #print(f'编辑框架子控件数量: {len(edit_frame.winfo_children())}')
            
            # 查找文本控件并处理内容
            text_widget = self._find_text_widget(edit_frame)
            if text_widget:
                # 获取文本内容
                text = text_widget.get("1.0", tk.END).strip()
                if text:
                    # 统一在此处处理文本输入
                    self._process_text_input(item_id, text)
        except Exception as e:
            # 如果发生错误，记录错误
            print(f'清理编辑器时出错: {str(e)}')
            import traceback
            traceback.print_exc()
            pass
        
        edit_frame.destroy()
        self.is_editing = False
        # 选中当前行
        self.tree.selection_set(item_id)
        # 设置焦点回到表格，以便可以立即响应回车事件
        self.tree.focus_set()
        
    def _find_text_widget(self, parent):
        """递归查找文本控件"""
        for child in parent.winfo_children():
            #print(f'查找子控件: {type(child)}, ID: {id(child)}')
            if isinstance(child, scrolledtext.ScrolledText) or isinstance(child, tk.Text):
                return child
            # 递归查找子控件
            if hasattr(child, 'winfo_children') and child.winfo_children():
                #print(f'递归查找子控件的子控件，数量: {len(child.winfo_children())}')
                found = self._find_text_widget(child)
                if found:
                    return found
        return None
    
    def _on_root_click(self, event):
        """处理根窗口点击事件，检测点击编辑区域外的情况"""
        if self.is_editing:
            # 检查点击是否发生在Treeview上或其编辑子控件上
            widget = event.widget
            
            # 遍历控件层次结构，检查是否是Treeview或其编辑子控件
            is_in_tree_or_editor = False
            while widget:
                if widget == self.tree:
                    is_in_tree_or_editor = True
                    break
                widget = widget.master
            
            # 如果点击不在Treeview或其编辑子控件上，则取消编辑状态
            if not is_in_tree_or_editor:
                # 查找并清理所有编辑框架
                for child in self.tree.winfo_children():
                    if isinstance(child, ttk.Frame):
                        try:
                            # 从编辑框架的属性中获取正确的item_id
                            item_id = getattr(child, 'edit_item_id', '')
                            if not item_id:
                                # 如果没有保存的item_id，尝试从选中项目中获取
                                selected_items = self.tree.selection()
                                item_id = selected_items[0] if selected_items else ''
                            self._cleanup_text_editor(child, item_id)
                        except Exception:
                            pass
    def _create_threshold_editor(self, item_id):
        """创建阈值编辑器"""
        # 创建编辑窗口
        edit_window = Toplevel(self.root)
        edit_window.transient(self.root)
        edit_window.grab_set()
        
        # 计算窗口位置
        x = self.root.winfo_x() + 100
        y = self.root.winfo_y() + 100
        edit_window.geometry(f"200x80+{x}+{y}")
        
        # 创建阈值输入框
        threshold_entry = ttk.Entry(edit_window, font=self.text_font)
        threshold_entry.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
        
        # 获取当前值
        try:
            current_value = self.tree.item(item_id, 'values')[2]
            threshold_entry.insert(0, current_value)
        except (IndexError, TypeError):
            threshold_entry.insert(0, "0.70")
        
        threshold_entry.focus_set()
        try:
            threshold_entry.select_range(0, tk.END)
        except TclError:
            pass
        
        # 设置编辑状态标志
        self.is_editing = True
        
        # 保存并关闭编辑器
        def save_threshold_with_reset(event=None):
            try:
                value = float(threshold_entry.get().strip())
                # 限制阈值范围
                value = max(0.1, min(1.0, value))
                values = list(self.tree.item(item_id, 'values'))
                if len(values) >= 3:
                    values[2] = f"{value:.2f}"
                    self.tree.item(item_id, values=values)
            except ValueError:
                # 如果输入无效，则保持原值
                pass
            finally:
                edit_window.destroy()
                self.is_editing = False
        
        threshold_entry.bind("<Return>", save_threshold_with_reset)
        threshold_entry.bind("<Escape>", lambda e: edit_window.destroy())
        edit_window.bind("<FocusOut>", lambda e: edit_window.destroy())
    
    def on_tree_mouse_motion(self, event):
        """处理鼠标悬停事件，改变行背景色"""
        try:
            # 识别当前鼠标所在的行
            item = self.tree.identify_row(event.y)
            
            # 如果不在任何行上
            if not item:
                # 如果之前有悬停的行，恢复其原始样式
                if self.hover_item:
                    try:
                        # 检查项目是否存在
                        self.tree.item(self.hover_item)
                        # 移除hover标签
                        self.tree.item(self.hover_item, tags=[])
                    except tk.TclError:
                        # 项目不存在，忽略错误
                        pass
                    self.hover_item = None
                return
            
            # 如果鼠标移动到了新的行
            if item != self.hover_item:
                # 首先恢复之前悬停行的样式
                if self.hover_item:
                    try:
                        # 检查项目是否存在
                        self.tree.item(self.hover_item)
                        self.tree.item(self.hover_item, tags=[])
                    except tk.TclError:
                        # 项目不存在，忽略错误
                        pass
                
                # 检查当前行是否被选中
                is_selected = item in self.tree.selection()
                
                # 如果行未被选中，应用悬停样式
                if not is_selected:
                    try:
                        # 先清除之前的标签配置
                        self.tree.tag_configure('hover', background='')
                        # 重新配置hover标签
                        self.tree.tag_configure('hover', background='#f5f5f5')
                        # 应用标签到当前行
                        self.tree.item(item, tags=['hover'])
                    except tk.TclError:
                        # 项目不存在，忽略错误
                        pass
                
                # 更新当前悬停的行引用
                self.hover_item = item
        except Exception as e:
            print(f"鼠标悬停处理错误: {str(e)}")
            # 忽略可能的错误，确保程序继续运行
            pass
            
    def on_tree_click(self, event):
        """处理表格点击事件
        
        统一处理三种退出编辑模式的情况：
        1. 按下回车键
        2. 点击表格内空白处
        3. 点击表格外空白处
        
        无论点击表格内什么位置，只要当前处于编辑状态，都会先清理编辑状态。
        然后根据点击区域执行相应的逻辑。
        
        Args:
            event: Tkinter事件对象，包含点击位置等信息
        """
        try:
            region = self.tree.identify_region(event.x, event.y)
            
            # 如果处于编辑状态，无论点击表格内什么位置，都应该先清理编辑状态
            if self.is_editing:
                # 查找并清理所有编辑框架
                for widget in self.tree.winfo_children():
                    if isinstance(widget, ttk.Frame):
                        # 查找编辑框架对应的item_id（假设框架名称中包含item_id信息）
                        frame_name = widget.winfo_name()
                        if 'item_' in frame_name:
                            # 提取item_id（格式：item_I001_frame等）
                            item_id_part = frame_name.split('item_')[1].split('_')[0]
                            # 清理编辑框架并处理文本内容
                            self._cleanup_text_editor(widget, item_id_part)
                
            # 如果点击的是单元格区域，处理单元格点击逻辑
            if region == "cell":
                item_id = self.tree.identify_row(event.y)
                column = self.tree.identify_column(event.x)
                if not column:
                    return
                    
                self._process_tree_click(item_id, column)
        except Exception as error:
            print(f"表格点击事件处理错误: {str(error)}")
            
    def _process_tree_click(self, item_id, column):
        """处理表格点击事件的具体逻辑"""
        try:
            column_index = int(column[1:]) - 1
            column_names = ['select', 'search_text', 'threshold', 'result']
            
            if not (0 <= column_index < len(column_names)):
                return
                
            column_name = column_names[column_index]
            if column_name == 'select':
                self._toggle_select_column(item_id)
        except (ValueError, IndexError):
            pass
            
    def _toggle_select_column(self, item_id):
        """切换选择列的状态"""
        try:
            values = list(self.tree.item(item_id, 'values'))
            if len(values) > 0:
                values[0] = SYMBOL_UNCHECKED if values[0] == SYMBOL_CHECKED else SYMBOL_CHECKED  # 保留原来的符号用于显示
                self.tree.item(item_id, values=values)
        except Exception:
            pass
    
    def _get_column_name_from_event(self, event):
        """从事件中获取列名"""
        try:
            column = self.tree.identify_column(event.x)
            if not column:
                return None
                
            column_index = int(column[1:]) - 1  # 列标识符格式为 #1, #2 等
            column_names = ['select', 'search_text', 'threshold', 'result']
            
            if 0 <= column_index < len(column_names):
                return column_names[column_index]
        except (ValueError, IndexError):
            pass
        return None
        
    def on_tree_double_click(self, event):
        """处理表格双击事件"""
        try:
            region = self.tree.identify_region(event.x, event.y)
            if region == "cell":
                item_id = self.tree.identify_row(event.y)
                column_name = self._get_column_name_from_event(event)
                if column_name in ['search_text', 'threshold']:
                    self.edit_cell(item_id, column_name)
        except Exception as error:
            print(f"处理双击事件时出错: {str(error)}")
    
    def _set_all_selection_status(self, selected):
        """设置所有行的选中状态"""
        status = SYMBOL_CHECKED if selected else SYMBOL_UNCHECKED  # 保留原来的符号用于显示
        for item in self.tree.get_children():
            try:
                values = list(self.tree.item(item, 'values'))
                if values:
                    values[0] = status
                    self.tree.item(item, values=values)
            except Exception as error:
                print(f"更新选择状态时出错: {str(error)}")
                continue
    
    def select_all(self):
        """全选行"""
        self._set_all_selection_status(True)

    def invert_selection(self):
        """反选行"""
        for item in self.tree.get_children():
            try:
                values = list(self.tree.item(item, 'values'))
                # 切换选择状态
                if len(values) > 0:
                    if values[0] == SYMBOL_CHECKED:  # 保留原来的符号用于显示
                        values[0] = SYMBOL_UNCHECKED
                    else:
                        values[0] = SYMBOL_CHECKED  # 保留原来的符号用于显示
                    self.tree.item(item, values=values)
            except Exception as error:
                print(f"更新选择状态时出错: {str(error)}")
                continue
    
    def delete_row(self):
        """删除选中的行"""
        selected_items = []
        for item in self.tree.get_children():
            values = self.tree.item(item, 'values')
            if len(values) > 0 and values[0] == SYMBOL_CHECKED:  # 检查是否被选中
                selected_items.append(item)
        
        if not selected_items:
            messagebox.showinfo("提示", "请先选择要删除的行！")
            return
        
        for item in selected_items:
            self.tree.delete(item)
    
    def select_file(self):
        """选择视频文件"""
        path = filedialog.askopenfilename(
            title="选择视频文件",
            filetypes=[("视频文件", "*.mkv *.mp4 *.avi *.mov *.flv"), ("所有文件", "*.*")]
        )
        if path:
            self.video_path_var.set(path)
            self.reset_progress_bar()
            # 清空信息面板
            self.info_panel.config(state="normal")
            self.info_panel.delete("1.0", "end")
            self.info_panel.config(state="disabled")
            # 自动添加一行搜索条件，并进入编辑模式
            if not self.tree.get_children():
                self.add_row(edit=True)
            
            # 重置字幕源选择状态：默认选择内置字幕，清空外挂字幕文件
            self.subtitle_source_var.set("internal")
            self.external_subtitle_var.set("")
            
            # 探测并显示视频中的字幕流
            self._detect_subtitle_streams(path)
    
    def start_matching(self):
        """开始匹配字幕
        
        从用户选择的视频文件中提取字幕，然后使用用户定义的搜索条件
        在字幕中查找匹配内容。每个搜索条件可以有不同的匹配阈值。
        
        流程：
        1. 验证视频文件路径
        2. 读取所有选中的搜索条件（搜索文本和匹配阈值）
        3. 检查视频是否有内置字幕
        4. 禁用UI控件并更新进度条
        5. 异步处理每个搜索条件的匹配任务
        """
        video_path = self.video_path_var.get()
        
        if not video_path:
            messagebox.showwarning("提示", "请先选择视频文件！")
            return
        
        if not os.path.exists(video_path):
            messagebox.showerror("错误", "视频文件不存在！")
            return
        
        # 获取所有被选中的搜索条件
        search_items = []
        for item in self.tree.get_children():
            values = self.tree.item(item, 'values')
            if len(values) > 0 and values[0] == SYMBOL_CHECKED:  # 只处理被选中的行
                search_text = values[1].strip() if len(values) > 1 else ""
                if search_text:
                    threshold_text = values[2].strip() if len(values) > 2 else ""
                    try:
                        threshold = float(threshold_text) if threshold_text else self.threshold_var.get()
                    except ValueError:
                        threshold = self.threshold_var.get()
                    search_items.append((item, search_text, threshold))
        
        if not search_items:
            messagebox.showwarning("提示", "请先输入搜索内容！")
            return
        
        # 检查字幕源
        # 检查字幕源是否有效
        if not self._check_subtitle_source(video_path):
            return
        
        # 禁用UI控件
        self.set_ui_state(False)
        self.progress.config(mode='determinate', maximum=len(search_items), value=0)
        self.indeterminate_progress.start()
        self.status_label.config(text=f"匹配中: 0/{len(search_items)}")
        
        # 获取字幕源信息
        subtitle_source = self.subtitle_source_var.get()
        external_subtitle_path = self.external_subtitle_var.get() if subtitle_source == "external" else None
        
        # 获取用户选择的字幕流索引
        subtitle_stream_index = None
        if subtitle_source == "internal" and hasattr(self, 'subtitle_stream_combobox'):
            selected_text = self.subtitle_stream_combobox.get()
            if selected_text and ":" in selected_text:
                try:
                    # 从选择文本中提取索引数字
                    subtitle_stream_index = int(selected_text.split(":")[0])
                except ValueError:
                    subtitle_stream_index = None
        
        # 清空之前的匹配结果
        for item in self.tree.get_children():
            values = list(self.tree.item(item, 'values'))
            if len(values) > 3 and values[3]:  # 检查是否有匹配结果
                # 清空结果列
                values[3] = ''
                self.tree.item(item, values=values)
        
        # 开始匹配
        self.total_tasks = len(search_items)
        self.completed_tasks = 0
        
        self.append_info(f"开始在视频中匹配字幕: {video_path}")
        if subtitle_stream_index is not None:
            self.append_info(f"使用字幕流索引: {subtitle_stream_index}")
        
        # 处理每个搜索词
        for item, search_text, threshold in search_items:
            self.executor.submit(self._match_task_wrapper, video_path, item, search_text, threshold, subtitle_source, external_subtitle_path, subtitle_stream_index)
    
    def _match_task_wrapper(self, video_path, item_id, search_text, threshold, subtitle_source="internal", external_subtitle_path=None, subtitle_stream_index=None):
        """匹配任务包装器，确保UI更新在主线程中进行
        
        在线程池中执行字幕匹配的耗时操作，但确保所有UI更新操作都
        在主线程中进行，避免多线程访问Tkinter组件导致的问题。
        
        Args:
            video_path (str): 视频文件路径
            item_id (str): 搜索条件在表格中的行ID
            search_text (str): 要搜索的文本内容
            threshold (float): 匹配阈值，范围0-1
            subtitle_source (str): 字幕源类型，"internal"(内置)或"external"(外挂)
            external_subtitle_path (str): 外挂字幕文件路径
            subtitle_stream_index (int, optional): 指定要提取的字幕流索引
        """
        try:
            # 在后台线程中执行耗时操作
            self._perform_subtitle_match(video_path, item_id, search_text, threshold, subtitle_source, external_subtitle_path, subtitle_stream_index)
        except Exception as error:
            # 确保错误处理也在主线程中进行
            self.root.after(0, self._handle_match_error, item_id, search_text, threshold, error)
        finally:
            # 确保进度更新在主线程中进行
            self.root.after(0, self._update_progress_in_main_thread)
            
    def _update_progress_in_main_thread(self):
        """在主线程中更新进度条和状态
        
        由_match_task_wrapper调用，确保进度更新操作在主线程中执行，
        避免多线程访问Tkinter组件导致的问题。
        """
        self.completed_tasks += 1
        self.update_progress()
        
    def _perform_subtitle_match(self, video_path, item_id, search_text, threshold, subtitle_source="internal", external_subtitle_path=None, subtitle_stream_index=None):
        """执行字幕匹配的核心逻辑
        
        从缓存或文件中获取字幕，然后使用智能匹配算法查找与搜索文本相似的
        字幕内容。
        
        Args:
            video_path (str): 视频文件路径
            item_id (str): 搜索条件在表格中的行ID
            search_text (str): 要搜索的文本内容
            threshold (float): 匹配阈值，范围0-1
            subtitle_source (str): 字幕源类型，"internal"(内置)或"external"(外挂)
            external_subtitle_path (str): 外挂字幕文件路径
            subtitle_stream_index (int, optional): 指定要提取的字幕流索引
        """
        # 获取或提取字幕
        cached_subtitles = self._get_or_extract_subtitles(video_path, item_id, search_text, threshold, subtitle_source, external_subtitle_path, subtitle_stream_index)
        if cached_subtitles is None:
            return  # 已在_get_or_extract_subtitles中处理了错误
            
        # 搜索字幕
        window_size = self.window_size_var.get()
        matches = self.clipper.search_subtitles_universal(cached_subtitles, search_text, threshold, window_size=window_size)
        
        # 处理匹配结果
        if matches:
            self._handle_successful_match(item_id, search_text, threshold, matches)
        else:
            self._handle_no_match(item_id, search_text, threshold)
            
    def _get_or_extract_subtitles(self, video_path, item_id, search_text, threshold, subtitle_source="internal", external_subtitle_path=None, subtitle_stream_index=None):
        """获取缓存字幕或提取新字幕
        
        首先尝试从缓存中获取字幕，如果没有缓存或缓存无效，则根据字幕源类型选择不同的方式获取字幕。
        提取的字幕会被解析并缓存起来供后续使用。
        
        Args:
            video_path (str): 视频文件路径
            item_id (str): 搜索条件在表格中的行ID
            search_text (str): 要搜索的文本内容
            threshold (float): 匹配阈值
            subtitle_source (str): 字幕源类型，"internal"(内置)或"external"(外挂)
            external_subtitle_path (str): 外挂字幕文件路径
            subtitle_stream_index (int, optional): 指定要提取的字幕流索引
            
        Returns:
            list or None: 解析后的字幕列表，如果发生错误则返回None
        """
        # 生成缓存键
        cache_key = self.clipper._generate_cache_key(video_path, subtitle_source, external_subtitle_path, subtitle_stream_index)
            
        # 首先尝试获取缓存的字幕
        cached_subtitles = self.clipper.get_cached_subtitles(cache_key)
        
        if cached_subtitles is not None:
            return cached_subtitles
            
        # 根据字幕源类型获取字幕
        subtitles_list = None
        
        if subtitle_source == "internal":
            # 从视频中提取字幕
            with tempfile.TemporaryDirectory() as temp_dir:
                subtitle_path, error = self.clipper.extract_subtitles(video_path, temp_dir, subtitle_stream_index)
                if error:
                    self.tree.item(item_id, values=(self.tree.item(item_id, 'values')[0], search_text, 
                                                 f"{threshold:.2f}", f"错误: {error}"))
                    return None
                
                try:
                    subtitles_list = VideoClipper._parse_subtitles(subtitle_path)
                except Exception as error:
                    self.tree.item(item_id, values=(self.tree.item(item_id, 'values')[0], search_text, 
                                                 f"{threshold:.2f}", f"解析错误: {str(error)}"))
                    return None
        elif subtitle_source == "external" and external_subtitle_path:
            # 直接读取外挂字幕文件
            try:
                subtitles_list = VideoClipper._parse_subtitles(external_subtitle_path)
            except Exception as error:
                self.tree.item(item_id, values=(self.tree.item(item_id, 'values')[0], search_text, 
                                             f"{threshold:.2f}", f"外挂字幕解析错误: {str(error)}"))
                return None
        
        # 缓存字幕
        self.clipper.cache_subtitles(cache_key, subtitles_list)
        return subtitles_list
        
    def _handle_successful_match(self, item_id, search_text, threshold, matches):
        """处理成功匹配的情况
        
        当找到与搜索文本匹配的字幕时，更新表格显示最佳匹配结果，并在信息面板中显示匹配信息。
        
        Args:
            item_id (str): 搜索条件在表格中的行ID
            search_text (str): 搜索的文本内容
            threshold (float): 匹配阈值
            matches (list): 匹配结果列表，每个元素为(similarity, match_info)元组
        """
        # 显示最佳匹配（按相似度排序后的第一个结果）
        best_match = matches[0]
        similarity = best_match[0]  # 匹配相似度
        match_info = best_match[1]  # 匹配信息字典
        result_text = f"匹配成功 ({similarity:.2f}): {match_info['text']}"
        self.tree.item(item_id, values=(self.tree.item(item_id, 'values')[0], search_text, 
                                     f"{threshold:.2f}", result_text))
        self.append_info(f"{SYMBOL_SUCCESS}【{search_text}】匹配成功: 相似度 {similarity:.2f}")
        
    def _handle_no_match(self, item_id, search_text, threshold):
        """处理未找到匹配的情况
        
        当没有找到与搜索文本匹配的字幕时，更新表格显示未找到结果，并在信息面板中显示相应信息。
        
        Args:
            item_id (str): 搜索条件在表格中的行ID
            search_text (str): 搜索的文本内容
            threshold (float): 匹配阈值
        """
        self.tree.item(item_id, values=(self.tree.item(item_id, 'values')[0], search_text, 
                                     f"{threshold:.2f}", "未找到匹配内容"))
        self.append_info(f"{SYMBOL_WARNING}【{search_text}】未找到匹配的字幕内容")
        
    def _handle_match_error(self, item_id, search_text, threshold, error):
        """处理匹配过程中的错误
        
        当匹配过程中发生错误时，更新表格显示错误信息，并在信息面板中显示详细的错误描述。
        
        Args:
            item_id (str): 搜索条件在表格中的行ID
            search_text (str): 搜索的文本内容
            threshold (float): 匹配阈值
            error (Exception): 发生的异常对象
        """
        self.tree.item(item_id, values=(self.tree.item(item_id, 'values')[0], search_text, 
                                     f"{threshold:.2f}", f"处理错误: {str(error)}"))
        self.append_info(f"{SYMBOL_ERROR}【{search_text}】处理出错: {str(error)}")
    
    def update_progress(self):
        """更新进度条和状态标签
        
        更新进度条显示当前已完成的任务数量，并更新状态标签显示进度百分比。
        当所有任务完成时，重新启用UI控件并显示完成通知。
        """
        self.progress.config(value=self.completed_tasks)
        self.status_label.config(text=f"处理中: {self.completed_tasks}/{self.total_tasks}")
        
        if self.completed_tasks == self.total_tasks:
            self.indeterminate_progress.stop()
            self.status_label.config(text=f"完成: {self.completed_tasks}/{self.total_tasks}")
            self.set_ui_state(True)  # 重新启用UI控件
            messagebox.showinfo("完成", f"所有匹配任务处理完成！共处理 {self.total_tasks} 项。")
    
    def _start_processing(self, task_type, process_function, status_text):
        """开始处理过程的通用方法
        
        提取start_clipping和start_screenshot中的共同逻辑
        
        Args:
            task_type (str): 任务类型（"clip"或"screenshot"）
            process_function (callable): 处理函数
            status_text (str): 状态显示文本
        """
        video_path = self.video_path_var.get()
        
        if not video_path:
            messagebox.showwarning("提示", "请先选择视频文件！")
            return
        
        if not os.path.exists(video_path):
            messagebox.showerror("错误", "视频文件不存在！")
            return
        
        # 获取选中的搜索条件
        search_items = []
        for item in self.tree.get_children():
            values = self.tree.item(item, 'values')
            if len(values) > 0 and values[0] == SYMBOL_CHECKED:  # 只处理选中的行
                search_text = values[1].strip() if len(values) > 1 else ""
                if search_text:
                    # 尝试获取阈值，如果没有或无效则使用默认值0.7
                    try:
                        threshold = float(values[2]) if len(values) > 2 else 0.7
                    except ValueError:
                        threshold = 0.7
                    search_items.append((search_text, threshold))
        
        if not search_items:
            messagebox.showwarning("提示", f"请先选择要{status_text}的行！")
            return
        
        # 确定要处理该视频文件了
        self.append_info(f"开始{status_text}: {video_path}")

        # 检查字幕源
        if not self._check_subtitle_source(video_path):
            return
        
        # 禁用UI控件
        self.set_ui_state(False)
        self.progress.config(mode='determinate', maximum=len(search_items), value=0)
        self.indeterminate_progress.start()
        self.status_label.config(text=f"{status_text}中: 0/{len(search_items)}")
       
        # 启动处理线程
        self.total_tasks = len(search_items)
        self.completed_tasks = 0
        
        # 获取字幕源信息
        subtitle_source = self.subtitle_source_var.get()
        external_subtitle_path = self.external_subtitle_var.get() if subtitle_source == "external" else None
        
        # 获取用户设置的最大跨屏数
        window_size = self.window_size_var.get()
        
        for index, (search_text, threshold) in enumerate(search_items, 1):
            self.executor.submit(
                process_function, 
                video_path, 
                search_text, 
                self.clipper.result_queue,
                threshold,
                window_size,
                subtitle_source,
                external_subtitle_path,
                index,  # 搜索项序号
                len(search_items)  # 总搜索项数量
            )
        
        # 立即启动结果检查，不阻塞主线程
        self.check_results()

    def start_clipping(self):
        """开始视频剪辑过程
        
        根据用户选择的搜索条件和匹配阈值，对视频进行剪辑，提取包含
        匹配字幕的视频片段。剪辑过程在后台线程中异步执行，避免阻塞UI。
        
        流程：
        1. 验证视频文件路径
        2. 读取所有选中的搜索条件
        3. 检查字幕源
        4. 禁用UI控件并更新进度条
        5. 在后台线程中执行剪辑任务
        6. 启动结果检查机制，异步更新UI显示进度
        """
        self._start_processing("clip", self.clipper.process_clip_task, "剪辑")

    def start_screenshot(self):
        """开始截图过程
        
        根据用户选择的搜索条件，对视频进行截图，在字幕中间时间点
        截取单一画面。截图过程在后台线程中异步执行，避免阻塞UI。
        
        流程：
        1. 验证视频文件路径
        2. 读取所有选中的搜索条件
        3. 检查字幕源
        4. 禁用UI控件并更新进度条
        5. 在后台线程中执行截图任务
        6. 启动结果检查机制，异步更新UI显示进度
        """
        self._start_processing("screenshot", self.clipper.process_screenshot_task, "截图")
    
    def _check_subtitle_source(self, video_path):
        """检查字幕源是否有效
        
        共用的字幕源检查逻辑，用于验证视频的内置字幕或外挂字幕文件是否有效。
        
        Args:
            video_path (str): 视频文件路径
            
        Returns:
            bool: 字幕源有效返回True，无效返回False
        """
        subtitle_source = self.subtitle_source_var.get()
        external_subtitle_path = self.external_subtitle_var.get() if subtitle_source == "external" else None
        
        if subtitle_source == "internal":
            # 检查视频是否有内置字幕
            try:
                probe = safe_ffmpeg_probe(video_path)
                subtitle_streams = [stream for stream in probe['streams'] 
                                   if stream['codec_type'] == 'subtitle']
                
                if not subtitle_streams:
                    messagebox.showwarning("提示", "该视频没有内置字幕！请选择其他视频或使用外挂字幕文件。")
                    return False
            except (ffmpeg.Error, FileNotFoundError, Exception) as error:
                error_message = f"无法读取视频文件信息: {str(error)}"
                messagebox.showerror("错误", error_message)
                self.append_info(f"{SYMBOL_ERROR}{error_message}")
                return False
        elif subtitle_source == "external":
            # 检查外挂字幕文件
            if not external_subtitle_path or not os.path.exists(external_subtitle_path):
                messagebox.showwarning("提示", "请先选择有效的外挂字幕文件！")
                return False
        
        return True
        
    def on_tree_enter_press(self, event=None):
        """处理表格回车事件
        
        当用户在表格中按下回车键且有行获得焦点时，在当前行下方添加新行，并
        将焦点自动转移到新增行的搜索词输入框，方便用户连续添加搜索条件。
        
        Args:
            event (tk.Event, optional): 事件对象，包含键盘事件信息
            
        Returns:
            str: "break"，表示阻止事件继续传播
        """
        # 处理未使用的event参数，避免pylint警告
        _ = event
        # 检查是否处于编辑状态
        if not self.is_editing:
            # 检查是否有行获得焦点或被选中
            focused_item = self.tree.focus()
            selected_items = self.tree.selection()
            
            if focused_item or selected_items:
                # 添加新行并获取新行的ID
                new_item_id = self.add_row(edit=False)  # 不自动进入编辑模式，因为我们要手动控制
                # 延迟一下再进入编辑模式，确保UI已渲染
                self.root.after(100, self.edit_cell, new_item_id, 'search_text')
        return "break"  # 防止事件继续传播
        
    def on_subtitle_source_change(self, *args):
        """处理字幕源选择变化事件
        
        当用户选择不同的字幕源时，启用或禁用对应的UI控件。
        选择"外挂字幕文件"时，启用字幕文件选择控件；
        选择"视频内置字幕"时，禁用字幕文件选择控件，启用字幕流选择控件。
        切换字幕源时，清空之前的匹配结果。
        
        Args:
            *args: 事件参数（未使用）
        """
        # 检查并清空匹配结果
        has_results = False
        for item in self.tree.get_children():
            values = list(self.tree.item(item, 'values'))
            if len(values) > 3 and values[3]:  # 检查是否有匹配结果
                has_results = True
                # 清空结果列
                values[3] = ''
                self.tree.item(item, values=values)
        
        # 如果清空了结果，显示提示信息
        if has_results:
            self.info_panel.config(state="normal")
            self.info_panel.insert("end", "提示: 已清空之前的匹配结果，请重新开始匹配。\n")
            self.info_panel.config(state="disabled")
        
        # 更新UI控件状态
        if self.subtitle_source_var.get() == "external":
            self.external_subtitle_entry.config(state="normal")
            self.select_subtitle_button.config(state="normal")
            self.subtitle_stream_combobox.config(state="disabled")
        else:
            self.external_subtitle_entry.config(state="disabled")
            self.select_subtitle_button.config(state="disabled")
            # 如果选择了内置字幕，检查是否有视频文件并启用字幕流选择
            video_path = self.video_path_var.get()
            if video_path and os.path.exists(video_path):
                self.subtitle_stream_combobox.config(state="readonly")
            else:
                self.subtitle_stream_combobox.config(state="disabled")
    
    def _detect_subtitle_streams(self, video_path):
        """探测视频文件中的字幕流
        
        使用ffmpeg.probe探测视频文件中的所有字幕流，
        将字幕流信息添加到字幕流选择下拉列表中。
        
        Args:
            video_path (str): 视频文件路径
        """
        try:
            # 清空现有选项
            self.subtitle_stream_combobox['values'] = []
            self.subtitle_stream_combobox.set("")
            
            # 使用safe_ffmpeg_probe获取视频信息（避免控制台窗口弹出）
            probe = safe_ffmpeg_probe(video_path)
            streams = probe.get('streams', [])
            
            # 收集字幕流信息
            subtitle_streams = []
            for stream in streams:
                if stream.get('codec_type') == 'subtitle':
                    index = stream.get('index', '')
                    lang = stream.get('tags', {}).get('language', 'unknown')
                    title = stream.get('tags', {}).get('title', '')
                    
                    # 构建显示文本
                    display_text = f"{index}: "
                    if title:
                        display_text += f"{title} ({lang})"
                    else:
                        display_text += f"Subtitle {index} ({lang})"
                    
                    subtitle_streams.append((index, display_text))
            
            # 更新下拉列表
            if subtitle_streams:
                # 排序字幕流（按索引）
                subtitle_streams.sort(key=lambda x: int(x[0]))
                
                # 设置下拉列表值
                self.subtitle_stream_combobox['values'] = [text for idx, text in subtitle_streams]
                
                # 默认选择第一个字幕流
                self.subtitle_stream_combobox.current(0)
                
                # 启用下拉列表
                self.subtitle_stream_combobox.config(state="readonly")
                
                # 如果当前选择的是内置字幕，确保UI状态正确
                if self.subtitle_source_var.get() == "internal":
                    self.subtitle_stream_combobox.config(state="readonly")
            else:
                # 没有找到字幕流
                self.subtitle_stream_combobox.set("视频中未检测到字幕流")
                self.subtitle_stream_combobox.config(state="disabled")
                
                # 显示提示信息
                self.info_panel.config(state="normal")
                self.info_panel.insert("end", f"提示: 视频文件 '{os.path.basename(video_path)}' 中未检测到内置字幕流。\n")
                self.info_panel.config(state="disabled")
        except ffmpeg.Error as error:
            # 处理ffmpeg错误
            error_msg = f"探测字幕流时出错: {str(error.stderr, 'utf-8')}" if error.stderr else str(error)
            print(error_msg)
            self.subtitle_stream_combobox.set("探测字幕流失败")
            self.subtitle_stream_combobox.config(state="disabled")
        except Exception as error:
            # 处理其他错误
            error_msg = f"探测字幕流时出错: {str(error)}"
            print(error_msg)
            self.subtitle_stream_combobox.set("探测字幕流失败")
            self.subtitle_stream_combobox.config(state="disabled")
    
    def select_subtitle_file(self):
        """让用户选择外挂字幕文件
        
        打开文件对话框，允许用户选择SRT格式的字幕文件，
        将选择的文件路径设置到对应的文本框中，并验证字幕与视频的匹配性。
        """
        file_path = filedialog.askopenfilename(
            title="选择字幕文件",
            filetypes=[("字幕文件", "*.srt"), ("所有文件", "*.*")]
        )
        if file_path:
            video_path = self.video_path_var.get()
            
            # 如果用户已经选择了视频文件，检查字幕与视频的匹配性
            if video_path and os.path.exists(video_path):
                is_match, match_info = VideoClipper.check_subtitle_video_match(video_path, file_path)
                
                # 显示匹配信息并让用户确认
                if not is_match:
                    response = messagebox.askyesno(
                        "字幕匹配检查",
                        f"警告：检测到字幕可能与视频不匹配！\n{match_info}\n\n是否仍要使用该字幕文件？"
                    )
                    if not response:
                        return
                else:
                    # 如果匹配良好，仍然让用户确认
                    response = messagebox.askyesno(
                        "字幕匹配检查",
                        f"字幕与视频匹配检查结果：\n{match_info}\n\n确认使用该字幕文件吗？"
                    )
                    if not response:
                        return
                    
                    # 显示匹配成功的信息
                    self.append_info(f"{SYMBOL_SUCCESS}字幕文件 '{os.path.basename(file_path)}' 与视频匹配良好")
            
            # 设置字幕文件路径
            self.external_subtitle_var.set(file_path)
    
    def set_ui_state(self, enabled):
        """设置UI控件状态
        
        统一控制应用程序中所有交互控件的启用/禁用状态，
        在执行耗时操作时禁用UI，避免用户的误操作。
        
        Args:
            enabled (bool): 是否启用UI控件
        """
        state = "normal" if enabled else "disabled"
        self.select_file_button.config(state=state)
        self.clip_button.config(state=state)
        self.add_row_button.config(state=state)
        self.select_all_button.config(state=state)
        self.invert_button.config(state=state)
        self.delete_row_button.config(state=state)
        self.match_button.config(state=state)
        self.subtitle_source_internal_radio.config(state=state)
        self.subtitle_source_external_radio.config(state=state)
        
        # 处理字幕源相关控件的状态
        if enabled:
            # 启用时，根据当前字幕源选择来设置控件状态，与on_subtitle_source_change保持一致
            if self.subtitle_source_var.get() == "external":
                self.external_subtitle_entry.config(state="normal")
                self.select_subtitle_button.config(state="normal")
                self.subtitle_stream_combobox.config(state="disabled")
            else:
                self.external_subtitle_entry.config(state="disabled")
                self.select_subtitle_button.config(state="disabled")
                # 检查是否有视频文件并启用字幕流选择
                video_path = self.video_path_var.get()
                if video_path and os.path.exists(video_path):
                    self.subtitle_stream_combobox.config(state="readonly")
                else:
                    self.subtitle_stream_combobox.config(state="disabled")
        else:
            # 禁用时，统一禁用所有相关控件
            self.external_subtitle_entry.config(state="disabled")
            self.select_subtitle_button.config(state="disabled")
            self.subtitle_stream_combobox.config(state="disabled")
            
        
        # 对于ttk.Scale控件，确保正确设置状态
        if enabled:
            self.threshold_slider.state(["!disabled"])
        else:
            self.threshold_slider.state(["disabled"])
    
    def check_results(self):
        """检查和处理后台任务的执行结果
        
        异步检查后台线程池执行的剪辑任务结果，批量处理结果并更新UI显示。
        采用批量处理策略，减少界面更新频率，提高应用程序响应性能。
        
        工作流程：
        1. 从结果队列中获取已完成的任务结果（非阻塞方式）
        2. 批量处理结果，更新进度条和状态标签
        3. 根据任务类型（成功、错误、未找到）在信息面板显示相应信息
        4. 如果任务未全部完成，根据处理速度动态调整检查间隔，继续检查
        5. 当所有任务完成时，更新进度为100%，重新启用UI控件并显示完成通知
        """
        
        batch_results = []
        batch_size = 1  # 每处理batch_size个结果更新一次界面
        
        try:
            while len(batch_results) < batch_size:
                result = self.clipper.result_queue.get_nowait()
                batch_results.append(result)
                self.completed_tasks += 1
                # 更新进度条
                self.progress.config(value=self.completed_tasks)
                # 更新状态标签
                self.status_label.config(text=f"剪辑中: {self.completed_tasks}/{self.total_tasks}")
        except queue.Empty:
            pass
        
        # 批量处理结果
        for result_type, search_text, result_value in batch_results:
            # 将路径中的反斜杠替换为斜杠
            if result_value and isinstance(result_value, str):
                formatted_path = result_value.replace("\\", "/")
            else:
                formatted_path = result_value
                
            if result_type == "success":
                self.append_info(f"{SYMBOL_SUCCESS}【{search_text}】剪辑完成: {formatted_path}")
            elif result_type == "screenshot_success":
                self.append_info(f"{SYMBOL_SUCCESS}【{search_text}】截图完成: {formatted_path}")
            elif result_type == "notfound":
                self.append_info(f"{SYMBOL_WARNING}【{search_text}】未找到匹配的字幕内容")
            elif result_type == "error":
                self.append_info(f"{SYMBOL_ERROR}【{search_text}】出错: {result_value}")
            elif result_type == "screenshot_error":
                self.append_info(f"{SYMBOL_ERROR}【{search_text}】截图出错: {result_value}")
        
        if self.completed_tasks < self.total_tasks:         
            # 动态调整检查频率
            check_interval = max(100, 500 - len(batch_results) * 100)  # 结果越多检查越快
            self.root.after(check_interval, self.check_results)
        else:
            self.progress.config(value=self.completed_tasks)
            self.status_label.config(text=f"完成: {self.completed_tasks}/{self.total_tasks}")
            self.indeterminate_progress.stop()  # 停止不确定进度条
            self.set_ui_state(True)
            messagebox.showinfo("完成", f"所有剪辑任务处理完成！共处理 {self.total_tasks} 项。")
    
    # 在VideoClipperApp类中添加
    def __del__(self):
        """应用程序退出时清理资源
        
        在应用程序实例被销毁时自动调用，确保正确释放后台资源：
        1. 关闭线程池执行器，不等待所有任务完成
        2. 清除字幕缓存，释放内存空间
        
        这是Python的析构函数，在对象被垃圾回收前自动执行。
        """
        if hasattr(self, 'executor'):
            self.executor.shutdown(wait=False)  # 关闭线程池，不等待剩余任务
        if hasattr(self, 'clipper'):
            self.clipper.clear_cache()  # 清除字幕缓存

def main():
    """应用程序入口点"""
    root = tk.Tk()
    # 创建应用程序实例
    VideoClipperApp(root)
    root.mainloop()


if __name__ == "__main__":
    main()